exec

package
v0.2026247.2350 Latest Latest
Warning

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

Go to latest
Published: Sep 4, 2026 License: MIT Imports: 25 Imported by: 0

Documentation

Index

Constants

View Source
const ContainerInfraErrMarker = "container-setup infra failure"

ContainerInfraErrMarker is the stable substring the container executor stamps into a container-setup infra error, mirroring SignalKillErrMarker. probeWasContainerInfra (eventually.go), the check-box exit mapping, and the reporter all key on it — one literal, no parallel copies (R3).

View Source
const PodmanInfraExitCode = 125

PodmanInfraExitCode is podman's own "an error occurred in podman itself" exit code — container create/store/config failures the CLI reports before (or instead of) running the container command. It is the PRIMARY structured infra signal. (podman/libpod registerExitCode: 125 = "the error is with podman itself".)

View Source
const SignalKillErrMarker = "terminated by signal"

RunCaptureCmd is the shared output-capture helper. Identical behaviour to the pre-cutover testrun.go's runCapture (which lived on the now- deleted Executor interface): exit codes are NOT errors, only spawn failures are. Lives here so SSHExecutor / NestedExecutor implementations can share it without circular imports. SignalKillErrMarker is the stable substring every RunCaptureCmd caller stamps into a signal-kill error (see below). The check runner's probeWasKilled matches it to tell a probe that was KILLED before completing (infra interruption → re-attempt) from a probe that RAN and returned a failure (authoritative → never retried). One shared literal (R3).

View Source
const VenueLocal = "local"

VenueLocal is the stable Venue() identifier for the local host. Exported so install_ledger.go and tests can reference it without hard-coding the literal.

Variables

This section is empty.

Functions

func AppendHopForFlatPath

func AppendHopForFlatPath(chain spec.DeployExecutor, node *spec.FleetNode, flatPath, leaf string) (spec.DeployExecutor, error)

AppendHopForFlatPath stacks one executor hop so commands land inside `node`'s substrate. flatPath is the dotted path with dots replaced by underscores — the host-side container name suffix; leaf is the final path segment (the node's own key), used for a pod deployed STANDALONE inside a VM guest (which has no parent-path concept — see the pod case).

func BuildBuilderRunArgs

func BuildBuilderRunArgs(opts spec.BuilderRunOpts) []string

BuildBuilderRunArgs assembles the podman/docker run argv. Extracted for unit-testability — we assert the produced argv rather than actually spawning a container in tests.

func BuilderRun

func BuilderRun(ctx context.Context, opts spec.BuilderRunOpts) ([]byte, error)

BuilderRun runs the configured builder container. Returns the command's combined stdout+stderr on success; on failure, returns the output plus the exec error.

func ClassifyContainerInfraFailure

func ClassifyContainerInfraFailure(exitCode int, stderr string) (signature string, ok bool)

ClassifyContainerInfraFailure reports whether a NONZERO container-run/exec result is podman's OWN infra failure (the container never ran the check command), returning the matched signature (with provenance) for annotation. exit-125 is the primary signal; the stderr table is the secondary discriminator. A zero exit is never infra.

func ContainerChainFromDescriptor

func ContainerChainFromDescriptor(engine, containerName string) spec.DeployExecutor

ContainerChainFromDescriptor rebuilds the exact single-hop NestedExecutor deploykit.ContainerChain(engine, containerName) produces — the SAME shape, without sdk/kit importing sdk/deploykit (deploykit already imports kit; importing back would cycle). deploykit.ContainerChain itself calls this (R3 — one construction, not two). engine defaults to "podman" (matching ContainerChain's own JumpPodmanExec default) for any value other than "docker".

func ContainerInfraError

func ContainerInfraError(signature string, exitCode int, stderrPreview string) error

ContainerInfraError is the exported constructor for a classified CONTAINER-CREATE infra failure — the R44 Option-A box-mode creates ONE persistent container host-side, and its create failure is classified there (not through the per-step RunCapture seam). The caller returns this as the check's error so it exits the INFRA class (a plain error → exit 1), never checks-failed, and the marker makes it recognizable in logs.

func ContextWithExecutor added in v0.2026213.2047

func ContextWithExecutor(ctx context.Context, e *Executor) context.Context

ContextWithExecutor returns ctx carrying an in-proc *Executor. The host's in-proc dispatch (inprocProvider.InvokeWithExecutor) calls this before invoking a compiled-in plugin so the plugin's ExecutorForInvoke can reach the reverse channel without a broker. It is ALSO used by a host typed compiled-in seam call (e.g. spec.ProjectLoader.ResolveMergedDeployTree, #55 coneA Q2(1)) to thread the in-proc executor to a plugin-side impl that retrieves it via ExecutorFromContext — the same in-proc reverse-channel pattern, outside an Invoke.

func ContextWithHostStepDeps added in v0.2026213.2047

func ContextWithHostStepDeps(ctx context.Context, deps *HostStepDeps) context.Context

ContextWithHostStepDeps threads the live host-step deps onto ctx for a compiled-in class:step plugin's OpExecute handler to recover via HostStepDepsFromCtx.

func DescriptorFromExecutor

func DescriptorFromExecutor(exec spec.DeployExecutor) spec.VenueDescriptor

DescriptorFromExecutor is the pure INVERSE of VenueFromDescriptor: it derives a serializable spec.VenueDescriptor from an already-materialized, LIVE executor value. Three round-trippable shapes are recognized ("shell"/"ssh"/"container" — the last added K1-unblock W3 Unit B, see below); any OTHER concrete type (e.g. a genuinely composed multi-hop *NestedExecutor, which cannot be flattened into one descriptor tuple) returns the zero VenueDescriptor{} — callers treat that exactly like VenueFromDescriptor's own "" case (no venue; keep whatever executor is already in hand). This does NOT generalize to arbitrary N-hop composition.

The bed-regression fix this promotion serves (FIX ROUND, S3b follow-up): a NESTED external deploy's ancestor executor (deploykit.RootExecutorForDeployNode's "ssh" result, threaded core- side as spec.EmitOpts.ParentExec via the ancestor-chain walk in charly/fleet_add_cmd.go's deriveChildExecutorForPath) is ALWAYS a plain ShellExecutor/*SSHExecutor for a single hop into a vm guest — never a NestedExecutor — so it round-trips through this exact pair of functions. charly/unified_targets.go's pluginDeployTarget.Add uses this to convert that live ancestor executor into venue_json BEFORE dispatch, since a live Go interface value cannot itself cross the []byte wire to candy/plugin-fleet (mirrors candy/plugin-fleet's OWN identically-shaped former venueDescriptorFromExecutor, now deleted — R3, one function for both directions' callers).

The "container" arm (K1-unblock W3 Unit B) recognizes the ONE enumerable *NestedExecutor shape deploykit.ContainerChain always produces — Parent a plain ShellExecutor{}, a single JumpPodmanExec/JumpDockerExec hop, no ExtraArgs — the venue every plain pod/container check runs against (the check-runner family's most common case, needed so a plugin-constructed ContainerChain venue can round-trip to the host over InvokeProvider's VenueDescriptor seam). Any OTHER *NestedExecutor shape (a different Parent, JumpSSH/JumpVirshConsole, non-empty ExtraArgs — i.e. genuine multi-hop composition) falls through to the zero-value default, unchanged.

func EnsureCharlyInDeployVenue

func EnsureCharlyInDeployVenue(ctx context.Context, ve spec.DeployExecutor, charlyBin, hostVer string) (string, error)

EnsureCharlyInDeployVenue delivers the controller charly into a live spec.DeployExecutor. It gives every process-capable deployment the same Ansible-like bootstrap: keep an equal/newer packaged Charly, otherwise place the active controller binary at a versioned /tmp path and invoke that explicit path. It never changes PATH and never downgrades a target package. The underlying probe/compare/deliver decision remains ensureCharly (one copy).

func EnsureCharlyInGuest

func EnsureCharlyInGuest(ctx context.Context, ssh SSHArgs, charlyBin, hostVer, strategyRaw string) (string, error)

EnsureCharlyInGuest is the vm-deploy PrepareVenue-time coordinator: it layers the cloud-init charly_install.strategy over a HOST-SURFACE delivery (os/exec ssh/scp against the managed alias), because at PrepareVenue time the reverse channel has NOT yet served a guest executor. auto/scp: deliver the host charly when the guest's is absent/older; skip: verify presence only.

func HostCharlyIsNewer

func HostCharlyIsNewer(hostVer, venueVerOut string) bool

HostCharlyIsNewer reports whether the host charly (hostVer) is STRICTLY newer than a venue's charly (venueVerOut = raw `charly version` stdout). The single CalVer arbiter (R3): a venue version that is unparseable/absent → host wins (treated as older); both parse → strict CalVer compare (host newer → true, venue equal-or-newer → false, never downgraded); host unparseable → false (never clobber a venue charly on an unprovable claim).

func IsContainerInfraResult

func IsContainerInfraResult(message string) bool

IsContainerInfraResult reports whether a CheckResult/StepResult message carries the infra marker (the executor stamped a container-setup infra error into it, propagated verbatim by the verbs' `execution error: %v` formatting). Keyed on the ONE marker (R3).

func NestedContainerName

func NestedContainerName(path string) string

NestedContainerName maps a dotted deploy path (e.g. "stack.web") to the container/venue name form ("stack_web") used for nested-executor addressing.

func ResolveDeployChain

func ResolveDeployChain(roots map[string]spec.FleetNode, dotted string, root spec.DeployExecutor) (*spec.FleetNode, spec.DeployExecutor, error)

ResolveDeployChain walks `dotted` through `roots` (typically the merged deployment tree from ResolveMergedTreeViaExecutor) and returns the leaf FleetNode + a composed DeployExecutor chain that reaches it from `root`.

`root` is typically &ShellExecutor{} (the operator's host, or the harness-sandbox context the harness loop runs in). Pass nil to substitute ShellExecutor.

For each path segment, a single hop is added based on the node's target classification:

target: pod / container → NestedExecutor with JumpPodmanExec /
                          JumpDockerExec into "charly-<flat-path>".
                          Container name flattens dot-separated
                          paths to underscore-separated to remain
                          a legal podman container name.
target: vm              → plain SSHExecutor when the parent chain
                          is local (no wrapper overhead), otherwise
                          NestedExecutor with JumpSSH on top.
target: host            → no hop (host nodes share the parent
                          venue).
target: kubernetes      → error (kubernetes manifests are leaves; not
                          traversable as exec targets).

Returns clear errors with available-name hints when a segment fails to resolve.

func RootExecutorForDeployNode

func RootExecutorForDeployNode(node *spec.FleetNode) (spec.DeployExecutor, error)

RootExecutorForDeployNode selects the ROOT DeployExecutor for a `target: local` deployment node from its `host:` field — the single source of truth for "where does a local deploy's work run?", shared by `charly fleet add` (the local deploy target.Add) and `charly check live` (runLocalCheck) so neither re-implements the selection (R3):

host: ""  / "local"        → ShellExecutor{} (this machine, direct shell)
host: "<user>@<machine>"   → &SSHExecutor{User, Host, Port, …}
host: "<machine>" + user:  → SSHExecutor with User from node.User

It does NOT handle the nested-inside-a-parent case (opts.ParentExec); that stays in the local deploy target.Add because it's deploy-execution-specific. Returns ShellExecutor{} for a nil node.

func RunCaptureCmd

func RunCaptureCmd(cmd *exec.Cmd) (string, string, int, error)

func UserScopeBindMounts

func UserScopeBindMounts(hostHome string) (map[string]string, error)

UserScopeBindMounts returns the standard subdirs each user-scope builder needs bind-mounted: $HOME/.pixi, $HOME/.cargo, $HOME/.npm-global, $HOME/.cache/charly. Ensures the source dirs exist on the host (empty dirs — so podman's --mount doesn't reject them).

func UserScopeEnv

func UserScopeEnv(hostHome string) map[string]string

UserScopeEnv returns the env-var set that pixi/npm/cargo need to write into the bind-mounted directories rather than the builder's own home paths.

func VenueFromDescriptor

func VenueFromDescriptor(d spec.VenueDescriptor) (spec.DeployExecutor, error)

VenueFromDescriptor re-materializes a spec.VenueDescriptor into a real DeployExecutor — the decouple point that lets a substrate lifecycle plugin run out-of-process: it hands back a serializable venue description over the wire, and whichever side needs a LIVE executor (previously only the host, via charly/substrate_lifecycle_grpc.go's now-deleted venueFromDescriptor) re-materializes it locally. Promoted here (S3b, Unit-6 design) because the function has ZERO core-only dependency (pure sdk/kit + sdk/spec) and now has TWO callers that must construct the byte-identical executor from the same descriptor: the host (still re-materializing it for --verify / subsequent dispatch calls on the same target) and candy/plugin-fleet (materializing its OWN executor for the substrate's Execute call, immediately after driving that same substrate's OpPrepareVenue). R3 — one function, not a duplicate in each package.

func VmChildExecutor

func VmChildExecutor(parentExec spec.DeployExecutor, deployName string) (spec.DeployExecutor, error)

VmChildExecutor wraps parentExec with an SSH jump into the VM represented by this node. At the root (parentExec == nil or ShellExecutor), the child gets a plain SSHExecutor — no nesting overhead for the common case of a VM on localhost.

The SSH alias keys off the per-deploy DOMAIN IDENTITY (charly-<VmDomainIdentity(deployName)>), NOT node.From (the shared kind:vm entity) — `charly vm create <entity> --domain <deploy>` writes the managed stanza under `charly-<deploy>` (P33). Several beds may share one entity via `from:`, so an entity-keyed alias collides them on ONE stanza (the R10 defect where sibling beds both derived `charly-eval-vm`); keying by the deploy makes the alias distinct per bed and matches the stanza vm create actually wrote. A direct create (deploy == entity) resolves to `charly-<entity>` naturally, and VmDomainIdentity flattens a dotted member path consistently with the domain the lifecycle named (fleet_members.go's `vmDomainIdentity(memberKey)`).

func WaitForCloudInit

func WaitForCloudInit(ctx context.Context, ssh SSHArgs, poll PollFunc) error

WaitForCloudInit polls `sudo cloud-init status` (as root — charly guests have passwordless sudo) until cloud-init reaches an EXPLICIT terminal status (done/error/disabled) over a SURVIVING ssh connection — that is the deterministic signal that first-boot host-key regen finished (sshd stable) AND the seed-package phase (which holds the distro package lock) completed. Ready ONLY on a terminal token: a blank/transient/"running" read keeps polling, so the deploy's first `pacman -Sy` can't race cloud-final.service. Only meaningful for cloud-image sources; skip for bootc with no cidata ISO.

func WaitForContainerReady

func WaitForContainerReady(bed string)

WaitForContainerReady gates on the container being exec-able AND its supervisord-managed children having left their transitional states, so a one-shot check-live port/service probe never races a child that has not yet bound. `charly start` returns when systemd reports the service active, but supervisord's autostart children are still STARTING for a moment after. This polls `supervisorctl status` until no child is STARTING/BACKOFF (a child binds its port the instant it reaches RUNNING) instead of sleeping a fixed, host-tuned interval. Images without supervisord settle immediately. Best-effort: silent on timeout (the next check-live step surfaces the real failure). Reads the project's readiness bounds via poll.ReadinessProvider() — the SAME plugin-portable channel every other executor wait uses (the host threads its resolved bounds via ResolvedReadiness.PluginEnv; a project-unaware caller falls back to the built-in defaults).

func WaitForPackageLock

func WaitForPackageLock(ctx context.Context, ssh SSHArgs, poll PollFunc) error

WaitForPackageLock makes the guest package manager READY for the deploy's own pacman/apt/dnf: (1) block until no boot-time package PROCESS runs (the direct R4 sync primitive on the contended lock, process-based so a stale lock can't hang it forever), then (2) clear a STALE lock FILE that a reaped boot-time install left with no holding process (re-checking no-process inside the command keeps the removal safe).

func WaitForSSH

func WaitForSSH(ctx context.Context, ssh SSHArgs, poll PollFunc) error

WaitForSSH polls `ssh <alias> true` until sshd answers (BINARY/EDGE readiness: refused→up), under the injected poll's bounds. ConnectTimeout=2 bounds each connect; ServerAlive* bound a connected-then-blackholed session; the injected poll's per-attempt context is the never-hang bound.

func WaitForVmSshReady

func WaitForVmSshReady(domainID string)

WaitForVmSshReady gates on the VM being SSH-reachable AND cloud-init having settled, using the SAME deterministic SSHExecutor preflight the VM check-live path and the external vm deploy walk run — NOT a fixed sleep. WaitForSSH polls until sshd answers; WaitForCloudInit retries until an ssh connection survives a `cloud-init status` poll (the deterministic cloud-init-settled signal — so deploy-add never races a still-running first-boot pacman). domainID is the per-deploy DOMAIN IDENTITY (the bed/member deploy name), not the shared kind:vm entity — the alias the create path published. Best-effort: silent on timeout (the downstream deploy-add surfaces the real error).

Types

type CharlyInstallStrategy

type CharlyInstallStrategy string

CharlyInstallStrategy is the resolved charly_install.strategy: "auto" | "scp" | "skip".

const (
	CharlyInstallAuto CharlyInstallStrategy = "auto"
	CharlyInstallScp  CharlyInstallStrategy = "scp"
	CharlyInstallSkip CharlyInstallStrategy = "skip"
)

func ResolveCharlyInstallStrategy

func ResolveCharlyInstallStrategy(raw string) CharlyInstallStrategy

ResolveCharlyInstallStrategy applies the default to a raw charly_install.strategy string.

type Executor added in v0.2026213.2047

type Executor struct {
	// contains filtered or unexported fields
}

Executor is the plugin-side handle to the host's live DeployExecutor over the E3b reverse channel. An out-of-process deploy/step/builder plugin runs shell/SSH ops on the real venue by calling these; the host executes them with the executor it stood up on the broker for this Invoke. The plugin never holds the (unmarshallable) executor itself.

func ExecutorForInvoke added in v0.2026213.2047

func ExecutorForInvoke(ctx context.Context, brokerID uint32) (*Executor, error)

ExecutorForInvoke resolves the host executor for a plugin's Invoke, transport-invisibly: an IN-PROC compiled-in plugin gets it from the context (ContextWithExecutor); an OUT-OF-PROCESS plugin falls back to the go-plugin broker id in its InvokeRequest. Plugin Invoke code calls this ONE accessor and works in either placement unchanged.

func ExecutorFromContext added in v0.2026213.2047

func ExecutorFromContext(ctx context.Context) (*Executor, bool)

ExecutorFromContext returns the in-proc *Executor carried on ctx, if any. The public counterpart to ContextWithExecutor: a typed compiled-in seam impl (e.g. the loader plugin's ResolveMergedDeployTree) retrieves the host-threaded in-proc executor through this accessor instead of receiving it as a parameter — keeping the seam signature spec-typed (the *Executor is an exec type the spec seam cannot name) while using the SAME in-proc reverse-channel path ExecutorForInvoke already established for Invoke. Returns ok=false when ctx carries no executor (a non-compiled-in placement, or a host caller that forgot ContextWithExecutor).

func ExecutorFromInvoke added in v0.2026213.2047

func ExecutorFromInvoke(brokerID uint32) (*Executor, error)

ExecutorFromInvoke dials the host's ExecutorService using the broker id the host passed in InvokeRequest.executor_broker_id. Errors if this plugin was not served over go-plugin (no broker) or the id is 0 (no executor attached — a verb/kind op, or a deploy op the host ran in-proc).

func NewInProcExecutor added in v0.2026213.2047

func NewInProcExecutor(client pb.ExecutorServiceClient) *Executor

NewInProcExecutor wraps an in-proc pb.ExecutorServiceClient (an adapter delegating DIRECTLY to the host's executorReverseServer, no socket) as an *Executor — the IN-PROCESS twin of the go-plugin broker path in ExecutorFromInvoke. It is what lets a COMPILED-IN plugin reach the SAME reverse-channel seam (HostBuild / RunHostStep / …) an out-of-process plugin dials over gRPC: the host threads the resulting *Executor onto the Invoke context (ContextWithExecutor), and the plugin picks it up via ExecutorForInvoke — so the plugin's Invoke code is byte-identical in both placements (placement-invisible above the registry, the whole point of the plugin abstraction).

func (*Executor) DescribeProvider added in v0.2026213.2047

func (e *Executor) DescribeProvider(ctx context.Context, class, word string) (found bool, contract *spec.StepContract, err error)

DescribeProvider asks the host for the CACHED capability metadata of another provider by (class, word) — no live Describe round-trip, no Invoke (K5-A item 2). found=false means no CONNECTED provider resolves (class, word); a plugin routing on this can distinguish that from a transport failure, which the returned error alone would not. contract is nil unless the resolved provider is class="step" and declares one (F3) — nil for every other class, exactly mirroring ProvidedCapability.step_contract's own optionality.

func (*Executor) GetFile added in v0.2026213.2047

func (e *Executor) GetFile(ctx context.Context, path string, asRoot bool) ([]byte, error)

GetFile reads a venue file back to the host (asRoot reads via sudo) — the check-verb artifact-pull leg (a screenshot / recording produced on the venue).

func (*Executor) HostBuild added in v0.2026213.2047

func (e *Executor) HostBuild(ctx context.Context, kind string, spec []byte) ([]byte, error)

HostBuild asks the host to run the registered host-builder for kind (F10 host-build) with the opaque spec, returning the builder's opaque result JSON. A non-empty reply error is a build failure (the RPC itself succeeded).

func (*Executor) InvokeProvider added in v0.2026213.2047

func (e *Executor) InvokeProvider(ctx context.Context, class, word, op string, params, env []byte, opts ops.InvokeProviderOpts) ([]byte, error)

InvokeProvider asks the host to invoke ANOTHER provider (class, word, op) on this plugin's behalf (F10 plugin↔plugin) — the host resolves it in the registry (lazily connecting it from the project's candy closure on a miss, S2) and Invokes it, returning the raw result JSON. params/env are the op's plugin_input / env (nil for none). opts carries the optional S1 venue descriptor + the optional S3b canonical-ref fallback; the zero value ops.InvokeProviderOpts{} reproduces the pre-S1 behavior exactly.

func (*Executor) PutFile added in v0.2026213.2047

func (e *Executor) PutFile(ctx context.Context, remotePath string, content []byte, mode uint32, ownerRoot bool) error

PutFile places file content at a path on the venue — the deploy/step file-PLACEMENT leg. An out-of-process deploy/step plugin that EXECUTES an InstallPlan's steps ships the bytes (a service unit, an env.d file, the charly binary, a builder artifact); the host materializes them and delegates to the live DeployExecutor.PutFile. ownerRoot == true installs the file as root:root (root-owned system paths); mode is the octal permission bits. Binary-safe (proto bytes). A non-empty reply error is the placement failure on the venue.

func (*Executor) RunCapture added in v0.2026213.2047

func (e *Executor) RunCapture(ctx context.Context, script string) (stdout, stderr string, exit int, err error)

RunCapture runs a command on the venue and returns stdout/stderr/exit separately — the check-verb capture leg (an out-of-process exec-based check verb probing the live container). A non-empty reply error is an EXECUTION failure, NOT a non-zero exit (which rides the returned exit code). Mirrors kit.Executor.RunCapture over the wire.

func (*Executor) RunHostStep added in v0.2026213.2047

func (e *Executor) RunHostStep(ctx context.Context, step spec.InstallStepView, optsJSON []byte) ([]spec.ReverseOp, error)

RunHostStep is the HOST-ENGINE channel leg (the generalization of the former F3 build channel): a deploy/step plugin walking an InstallPlan that hits one of the five step kinds it cannot execute itself — BuilderStep (podman / makepkg / the injected EnsureImage closure), LocalPkgInstallStep, SystemPackagesStep (the DistroConfig package-template render), an act-verb OpStep (a builtin ProvisionActor that needs the in-proc registry), or an ExternalPluginStep (a verb served by ANOTHER out-of-process plugin, dispatched over a nested reverse channel) — drives this. The host reconstructs the step, runs the existing in-core machinery on the host, applies the effect onto the venue, and returns the step's recorded reverse ops. The plugin folds them into its DeployReply (sdk.BuildDeployReply) so `charly fleet del` replays them (record-and-replay teardown). The plugin owns the plan WALK; the host owns the host ENGINE. A non-nil error is a host-engine/apply FAILURE on the venue.

func (*Executor) RunInteractive added in v0.2026213.2047

func (e *Executor) RunInteractive(ctx context.Context, script string) (int, error)

RunInteractive runs a command on the venue wired to the operator's LIVE TTY (F12): the HOST reverse-server runs it inheriting os.Stdin/os.Stdout/os.Stderr (= the operator's terminal), so stdio never crosses the wire — only script→exit. Blocks until the session ends. The live-stdio sibling of RunCapture (consumers: charly shell/-cmd via the plugin's OpAttach).

func (*Executor) RunStream added in v0.2026213.2047

func (e *Executor) RunStream(ctx context.Context, script string) (int, error)

RunStream runs a command on the venue streaming stdout/stderr LIVE to the operator (F12; no stdin). Blocks until exit. Consumer: charly logs --follow via the plugin's OpLogs.

func (*Executor) RunSystem added in v0.2026213.2047

func (e *Executor) RunSystem(ctx context.Context, script string, optsJSON []byte) error

RunSystem runs a root (sudo) script on the venue; optsJSON is a marshalled EmitOpts (nil for none). A non-empty reply error is the script's failure on the venue.

func (*Executor) RunUser added in v0.2026213.2047

func (e *Executor) RunUser(ctx context.Context, script string, optsJSON []byte) error

RunUser runs an unprivileged script on the venue (see RunSystem).

func (*Executor) Venue added in v0.2026213.2047

func (e *Executor) Venue(ctx context.Context) (string, error)

Venue returns the host executor's stable venue identifier.

func (*Executor) VenueCapture added in v0.2026213.2047

func (e *Executor) VenueCapture(ctx context.Context, script string) (string, error)

VenueCapture runs a command on the venue and returns stdout, surfacing stderr on a non-zero exit — an EXEC-based check verb's capture-or-fail helper over the reverse channel.

func (*Executor) VenueHasTool added in v0.2026213.2047

func (e *Executor) VenueHasTool(ctx context.Context, tool string) bool

VenueHasTool reports whether tool is on PATH on the venue — an EXEC-based check verb's tool-presence probe over the reverse channel.

func (*Executor) VenueRunSilent added in v0.2026213.2047

func (e *Executor) VenueRunSilent(ctx context.Context, script string) error

VenueRunSilent runs a command on the venue discarding output, returning an error on a non-zero exit — an EXEC-based check verb's fire-and-forget helper over the reverse channel.

type HostStepDeps added in v0.2026213.2047

type HostStepDeps struct {
	// Exec is the live typed venue executor (the broker's s.exec). deploykit.RunVenueBuilderStep
	// / ExecLocalPkgInstall / RenderHostPackageCommand's RunSystem all drive it.
	Exec spec.DeployExecutor
	// ResolveImage / EnsureImage are the image-resolve/ensure closures the BuilderStep deploy
	// leg injects into deploykit.RunVenueBuilderStep. They close over the host *Config +
	// ProjectDir (charly-internal — coneK1b's #8 keeps resolveImageRefForEnsure's definition;
	// the broker constructs the closures, the plugin only invokes them).
	ResolveImage func(string) (string, error)
	EnsureImage  func(context.Context, string) error
	// DistroCfg is the resolved distro: vocabulary the SystemPackagesStep host render
	// (deploykit.RenderHostPackageCommand) looks up the format's phase.install.host template in.
	DistroCfg *spec.DistroConfig
	// Opts are the deploy EmitOpts (DryRun / AllowRepoChanges / SkipIncompatible / …) the
	// deploy-leg bodies take.
	Opts spec.EmitOpts
}

HostStepDeps carries the live, non-serializable inputs a compiled-in class:step plugin needs to run a deploy-leg host-engine step body (Builder / LocalPkgInstall / SystemPackages) on the host venue. Set by the wire broker before InvokeProvider(ClassStep, <word>, OpExecute); recovered by the plugin's OpExecute handler. IN-PROC-ONLY (the typed executor + closures cannot cross the wire — candy/plugin-installstep is compiled-in).

func HostStepDepsFromCtx added in v0.2026213.2047

func HostStepDepsFromCtx(ctx context.Context) *HostStepDeps

HostStepDepsFromCtx recovers the threaded host-step deps (nil when absent — an out-of-process placement, or a ctx that never carried them).

type JumpKind

type JumpKind int

JumpKind classifies how NestedExecutor enters the child environment. This determines both the shell-wrapper syntax for RunSystem/RunUser and the file-transport strategy for PutFile.

const (
	// JumpPodmanExec enters a rootful or rootless podman container
	// via `podman exec -i <name>`. The parent must have podman
	// available (the container-nesting candy provides this for
	// container-in-container; the virtualization candy is unrelated
	// here — container children of container parents).
	JumpPodmanExec JumpKind = iota + 1

	// JumpDockerExec enters a docker container via `docker exec -i`.
	// Separate from podman because docker and podman CLIs differ in
	// exit-code propagation and stdin handling.
	JumpDockerExec

	// JumpSSH makes an additional ssh hop — used when the child
	// itself is an SSH-reachable VM inside an already-SSH-reachable
	// parent (vm-in-container, vm-in-vm).
	JumpSSH

	// JumpVirshConsole attaches to a libvirt guest's serial console
	// for emergency-only shell access (used when the guest is
	// unreachable over SSH). Best-effort; primarily a diagnostic path.
	JumpVirshConsole
)

type NestedExecutor

type NestedExecutor struct {
	Parent spec.DeployExecutor
	Jump   NestedJump
}

NestedExecutor wraps a parent DeployExecutor and rewrites each DeployExecutor primitive to run inside a nested environment via Jump. Stacks: a NestedExecutor can itself be the Parent of another NestedExecutor, yielding arbitrary-depth composition.

func (*NestedExecutor) GetFile

func (n *NestedExecutor) GetFile(ctx context.Context, remotePath string, asRoot bool, opts spec.EmitOpts) ([]byte, error)

GetFile retrieves a file from the nested environment. Current implementation is a minimal read-then-retrieve: the parent shell runs `<jump> cat <path>` and the output is captured via a staged tmp file on the parent venue, then Parent.GetFile pulls it back. Jumps without an obvious cat+stdout path (libvirt-console) return an explicit "not supported" error rather than silently producing empty bytes.

func (*NestedExecutor) Kind

func (n *NestedExecutor) Kind() string

Kind reports the venue's coarse classification, derived from the LEAF jump (this NestedExecutor's own Jump). The parent chain doesn't affect Kind because tests care about what their probe lands in, not the path taken to reach it.

func (*NestedExecutor) PutFile

func (n *NestedExecutor) PutFile(ctx context.Context, localPath, remotePath string, mode uint32, ownerRoot bool, opts spec.EmitOpts) error

PutFile transfers a file from the invoking host into the nested venue. Implementation picks a FileTransport based on Jump.Kind: podman/docker containers use `<engine> cp`, SSH hops use scp, libvirt console uses virt-copy-in (best effort).

For stacked jumps, the transfer is multi-stage: first land the file at a tmp path on the parent venue (via Parent.PutFile), then issue a one-shot move-into-child command via RunSystem. This preserves the "every executor only knows its own hop" invariant and keeps the FileTransport implementation simple.

func (*NestedExecutor) ResolveHome

func (n *NestedExecutor) ResolveHome(ctx context.Context, user string) (string, error)

ResolveHome returns $HOME for `user` inside the leaf environment of this nested executor. Implementation: shell out a small `getent passwd` command via RunCapture (which already routes through the jump chain). Empty user resolves to the leaf-shell's $HOME.

func (*NestedExecutor) RunBuilder

func (n *NestedExecutor) RunBuilder(ctx context.Context, opts spec.BuilderRunOpts) ([]byte, error)

RunBuilder always runs on the outermost host (where podman with image-building capabilities lives). Delegates to the parent chain unchanged — nested executors never run builders inside themselves. Artifacts produced by the builder land on the outer host and are distributed to the nested environment via PutFile.

func (*NestedExecutor) RunCapture

func (n *NestedExecutor) RunCapture(ctx context.Context, script string) (string, string, int, error)

RunCapture executes a script inside the nested venue and returns stdout/stderr/exit. The wrapped script is forwarded to the parent's own RunCapture, so multi-hop chains compose: harness sandbox (host) → VM (ssh) → inner-pod (podman exec) → nested-pod (podman exec) stacks four heredocs and captures the deepest stdout.

asRoot is hard-coded to false here: test verbs probe state and add `sudo` explicitly when needed (matching the pre-cutover Executor.Exec semantics). Adding root escalation would silently change probe behaviour for every existing test.

func (*NestedExecutor) RunInteractive

func (n *NestedExecutor) RunInteractive(ctx context.Context, script string) (int, error)

func (*NestedExecutor) RunStream

func (n *NestedExecutor) RunStream(ctx context.Context, script string) (int, error)

func (*NestedExecutor) RunSystem

func (n *NestedExecutor) RunSystem(ctx context.Context, script string, opts spec.EmitOpts) error

RunSystem routes a POSIX-shell script through the jump as root. The far-side interpreter is bash wherever bash exists and sh on a busybox base — the script body must not assume bashisms (see jumpShell / nestedShellProbe).

func (*NestedExecutor) RunUser

func (n *NestedExecutor) RunUser(ctx context.Context, script string, opts spec.EmitOpts) error

RunUser runs as the unprivileged user inside the nested environment.

func (*NestedExecutor) StartProcess

func (n *NestedExecutor) StartProcess(ctx context.Context, launch spec.ProcessLaunch) (spec.Process, error)

StartProcess composes the leaf argv into one argv for the parent process executor. This makes arbitrary container/SSH nesting recursive and keeps the gRPC transport unaware of deployment kinds.

func (*NestedExecutor) Venue

func (n *NestedExecutor) Venue() string

Venue returns "nested:<Jump>/<parentVenue>". The parent venue is appended so the full chain is preserved — useful for ledger keying and debug output (e.g. "nested:podman-exec:mychild/ssh://arch@…").

type NestedJump

type NestedJump struct {
	Kind   JumpKind
	Target string

	// User is the explicit `--user` value (e.g. "1000:1000") passed to a
	// podman/docker exec hop. When set, the exec argv carries `--user <User>`
	// so the session user is deterministic instead of whatever the engine's
	// exec user/HOME resolution derives at that moment (issue #149: a
	// container created during a concurrent bed window resolves uid=1000
	// gid=0 with an empty HOME even though the OCI spec carries 1000:1000
	// and HOME=/home/user). Filled from the running container's .Config.User
	// at chain construction; empty means "let the engine resolve" (the
	// pre-fix behavior).
	User string

	// Home is the explicit HOME value (e.g. "/home/user") passed to a
	// podman/docker exec hop as `--env HOME=<Home>`. See User.
	Home string

	// Extra arguments inserted before the shell invocation. Rarely
	// needed; useful for `podman exec --env FOO=bar` or
	// `ssh -o ProxyJump=…` style tweaks.
	ExtraArgs []string
}

NestedJump describes one hop into a nested environment. The Target string's meaning depends on Kind:

JumpPodmanExec / JumpDockerExec: container name.
JumpSSH:                         "[user@]host[:port]" or an
                                 ssh-config alias (e.g.
                                 "charly-<vmname>"). ssh(1) reads
                                 ~/.ssh/config + agent for keys
                                 and connection options — we
                                 contain zero credential state.
JumpVirshConsole:                libvirt domain name.

func (NestedJump) String

func (j NestedJump) String() string

String renders a jump as a human-readable venue segment. Used as a component of NestedExecutor.Venue().

type PollFunc

type PollFunc func(ctx context.Context, cond poll.PollCondition) error

PollFunc drives a poll.PollCondition to readiness under the caller's readiness-configured bounds. Core and the vm plugin inject one wrapping vmshared's pollUntil + the resolved remote bounds, so kit never imports the readiness/poll subsystem.

type SSHArgs

type SSHArgs struct {
	User           string
	Host           string
	Port           int
	Args           []string
	ConnectTimeout int
}

SSHArgs is the host-surface ssh/scp coordinates for a managed VM alias.

func (SSHArgs) BaseArgs

func (a SSHArgs) BaseArgs() []string

BaseArgs builds the common ssh invocation prefix: only the per-call ergonomics (LogLevel, ConnectTimeout) + optional Port + pass-through Args + the "user@host"-or-"host" destination (ssh(1) reads ~/.ssh/config + ssh-agent for keys/host-key checking/identity).

func (SSHArgs) ScpBaseArgs

func (a SSHArgs) ScpBaseArgs() []string

ScpBaseArgs builds the scp invocation prefix (scp's `-P` is uppercase vs ssh's lowercase `-p`).

type SSHExecutor

type SSHExecutor struct {
	// User is the SSH login user. Optional — when empty, ssh(1) reads
	// the User directive from ~/.ssh/config or falls back to $USER.
	User string

	// Host is the SSH target — a hostname, an "[user@]host[:port]"
	// destination, or an ssh-config alias (e.g., the "charly-<vmname>"
	// stanzas managed by charly vm create). Required.
	Host string

	// Port is the SSH port. Optional — when 0, ssh uses the Port
	// directive from ~/.ssh/config or default 22.
	Port int

	// Args are extra ssh-cli arguments appended verbatim before the
	// destination. Pass-through from the deployment's `ssh_args:` field
	// (Ansible's ansible_ssh_extra_args). We do NOT parse, validate,
	// or interpret. Use sparingly — ssh-config is the right home for
	// persistent options.
	Args []string

	// ConnectTimeout caps the `-o ConnectTimeout=<N>` used in every
	// ssh invocation. Defaults to 10 seconds when zero.
	ConnectTimeout int
}

SSHExecutor implements DeployExecutor against an SSH-reachable guest. Used by the external vm deploy to run the same InstallPlan IR that the local deploy target runs — but wrapped as `ssh <user>@<host> sudo bash -s` instead of direct local bash, and scp for file transfers.

The builder-container path (VenueContainerBuilder steps for pixi/npm/cargo/aur) runs on the **host** (where podman is available), and the resulting artifacts are scp'd into the guest via PutFile. This keeps podman out of the guest's dependency surface.

Credential-free by design: SSHExecutor contains zero key paths, zero host-key overrides, zero ssh-agent socket detection. ssh(1) reads ~/.ssh/config and ssh-agent for everything. VMs publish a managed Host stanza into ~/.config/charly/ssh_config (Included from ~/.ssh/config) that names the IdentityFile + UserKnownHostsFile + StrictHostKeyChecking per VM; charly vm create writes the stanza, charly vm destroy removes it.

func SSHParamsForVm

func SSHParamsForVm(domainID string) *SSHExecutor

SSHParamsForVm returns an SSHExecutor pointing at the VM's managed ssh-config alias (charly-<domainID>) — the caller passes the per-deploy DOMAIN IDENTITY (VmDomainIdentity of the deploy), NOT the shared kind:vm entity (P33). All connection details — User, Port, IdentityFile, host-key checking — live in the Host stanza that `charly vm create` / `charly fleet add` published into ~/.config/charly/ssh_config; ssh(1) reads them from there. Our SSHExecutor needs only the alias as Host.

func (*SSHExecutor) GetFile

func (e *SSHExecutor) GetFile(ctx context.Context, remotePath string, asRoot bool, opts spec.EmitOpts) ([]byte, error)

GetFile retrieves the contents of a file from the guest via `ssh <host> [sudo] cat <path>` with stdout captured. asRoot==true wraps the read in sudo so restricted files (e.g. kubeconfig under /etc/rancher) are accessible from the unprivileged SSH user.

func (*SSHExecutor) Kind

func (e *SSHExecutor) Kind() string

Kind reports "ssh" — SSHExecutor targets any host reachable over SSH (VMs via the managed charly-<name> aliases, remote machines via "[user@]host[:port]" or ssh-config aliases).

func (*SSHExecutor) PutFile

func (e *SSHExecutor) PutFile(ctx context.Context, localPath, remotePath string, mode uint32, ownerRoot bool, opts spec.EmitOpts) error

PutFile copies a local file into the guest via scp, then `install`s it into place with the correct mode/owner. This two-step dance is needed because scp runs as the guest's unprivileged user (you can't scp directly to /usr/local/bin).

ownerRoot decides the PRIVILEGE the install runs at — the SAME contract as ShellExecutor.PutFile (R3): ownerRoot=true → `sudo install -o root -g root` (a system-scoped file, root-owned); ownerRoot=false → a plain user-scoped `install` run AS THE GUEST USER (RunUser, NO sudo) so the file AND any parent dirs `install -D` creates are USER-owned. Running the non-root branch under sudo (the old bug — it always RunSystem'd) created root-owned `~/.config/opencharly/{env.d,…}` in the guest, which then blocked the user-scoped ledger write (`mkdir … Permission denied`). The prior in-proc VM target wrote env.d via RunUser for exactly this reason; the externalized walk reaches the same RunUser path through here.

func (*SSHExecutor) ResolveHome

func (e *SSHExecutor) ResolveHome(ctx context.Context, user string) (string, error)

ResolveHome returns $HOME for `user` on the SSH-reachable target. Empty user resolves to whoever ssh logged in as (via `echo $HOME`), non-empty user goes through `getent passwd <user>` so callers can resolve any user's home on the guest.

This replaces the bug where `the local deploy target.HostHome` was initialized from `os.Getenv("HOME")` — the operator's home, not the guest user's. Any subsequent shell-rc edit (env.d sourcing block, new shell:-schema managed-block, etc.) now lands in the right place.

func (*SSHExecutor) RunBuilder

func (e *SSHExecutor) RunBuilder(ctx context.Context, opts spec.BuilderRunOpts) ([]byte, error)

RunBuilder delegates to BuilderRun which runs the podman container *on the host*. Caller (the external vm deploy) is responsible for scp-ing the resulting artifacts into the guest via PutFile afterwards — this executor doesn't shuttle artifact trees itself.

func (*SSHExecutor) RunCapture

func (e *SSHExecutor) RunCapture(ctx context.Context, script string) (string, string, int, error)

RunCapture executes a script on the guest and returns captured stdout/stderr/exit. Mirrors the deleted VmTestExecutor.Exec semantics: no automatic root escalation (callers that need root prefix sudo).

func (*SSHExecutor) RunInteractive

func (e *SSHExecutor) RunInteractive(_ context.Context, script string) (int, error)

func (*SSHExecutor) RunStream

func (e *SSHExecutor) RunStream(_ context.Context, script string) (int, error)

func (*SSHExecutor) RunSystem

func (e *SSHExecutor) RunSystem(ctx context.Context, script string, opts spec.EmitOpts) error

RunSystem executes a bash script as root on the guest. Wraps as `ssh vm 'sudo bash -s'` with the script fed on stdin.

func (*SSHExecutor) RunUser

func (e *SSHExecutor) RunUser(ctx context.Context, script string, opts spec.EmitOpts) error

RunUser executes a bash script as the guest's unprivileged user (i.e. spec.SSH.User, the account SSHExecutor connects as).

func (*SSHExecutor) SSHBaseArgs

func (e *SSHExecutor) SSHBaseArgs() []string

SSHBaseArgs builds the common ssh invocation prefix. ssh(1) reads ~/.ssh/config + ssh-agent for keys, host-key checking, identity files, etc. We supply only the per-call ergonomics (LogLevel, ConnectTimeout) plus optional Port (when caller pre-parsed it from the destination string) plus the deployment's pass-through Args. The destination is "user@host" when User is set, otherwise just Host — letting ssh-config's User directive apply.

func (*SSHExecutor) StartProcess

func (e *SSHExecutor) StartProcess(ctx context.Context, launch spec.ProcessLaunch) (spec.Process, error)

StartProcess launches one exact argv through SSH. OpenSSH necessarily sends a remote command string; POSIX single quoting each token preserves the argv boundary and prevents interpolation by the remote login shell.

func (*SSHExecutor) Venue

func (e *SSHExecutor) Venue() string

Venue returns a stable "ssh://<user>@<host>:<port>" identifier so install_ledger.go can scope per-target ledgers without colliding with the local-shell ledger or other SSH targets. Components that are empty stringify naturally ("ssh://server" when User+Port unset).

func (*SSHExecutor) WaitForCloudInit

func (e *SSHExecutor) WaitForCloudInit(ctx context.Context) error

WaitForCloudInit polls `sudo cloud-init status` on the guest until cloud-init settles (status done/error/disabled). Only meaningful for cloud-image VMs; callers should skip this for bootc sources with no cidata ISO attached.

func (*SSHExecutor) WaitForPackageLock

func (e *SSHExecutor) WaitForPackageLock(ctx context.Context) error

WaitForPackageLock makes the guest package manager READY for a deploy plan's own `pacman -Sy` / `apt` / `dnf`: (1) it blocks until no boot-time package PROCESS is running (cloud-init's first-boot seed-package install holds the lock through cloud-final.service — the direct R4 sync primitive on the contended resource), then (2) it clears a STALE lock FILE left with no holding process. A boot-time pacman is reaped by systemd when cloud-final.service's cgroup is torn down, leaving an empty `/var/lib/pacman/db.lck` that no process owns; pacman then refuses ("unable to lock database") even though nothing is running. Removing a lock NO process holds is the standard, safe stale-lock recovery — gated on a re-check of no-process inside the command so it never races a live transaction. Process-based (not lock-file-based) for the WAIT so a stale lock can't hang the gate forever.

func (*SSHExecutor) WaitForSSH

func (e *SSHExecutor) WaitForSSH(ctx context.Context) error

WaitForSSH polls the guest's sshd until it accepts connections (bounded by maxWaitSeconds). Returns nil on first successful connect, error on timeout. Used by the vm deploy's PrepareVenue right after `charly vm create`.

The loop uses a wall-clock deadline (not an iteration count) and a 1-second sleep between attempts. The previous design (300 iterations with no sleep) fast-failed in ~15 seconds when each SSH attempt errored quickly — connection-refused returns in ~50 ms, host-key-mismatch in ~20 ms — burning the entire "300-second budget" in a tiny window. With a 1 s sleep between attempts, slow-to-boot guests get the full polling window they were always supposed to receive. Fixed during the 2026-05-06 R10 follow-up.

type ShellExecutor

type ShellExecutor struct{}

ShellExecutor implements DeployExecutor against the invoking user's shell + filesystem. Faithful behavior-preserving wrapper around the existing runSudoShell / runUserShell / BuilderRun helpers.

func (ShellExecutor) GetFile

func (ShellExecutor) GetFile(ctx context.Context, remotePath string, asRoot bool, opts spec.EmitOpts) ([]byte, error)

GetFile on the local executor is a direct filesystem read. When asRoot is set, the read is delegated to `sudo cat` so files with restricted permissions (e.g. /etc/shadow, rancher kubeconfig) can still be retrieved. Stdout is captured verbatim.

func (ShellExecutor) Kind

func (ShellExecutor) Kind() string

Kind reports "host" — ShellExecutor's commands run on the operator's machine.

func (ShellExecutor) PutFile

func (ShellExecutor) PutFile(_ context.Context, localPath, remotePath string, mode uint32, ownerRoot bool, opts spec.EmitOpts) error

PutFile on the local executor is a direct filesystem write. When ownerRoot is set, the installer uses `sudo install -m <mode> -o root -g root` so the target path can be /usr/local/bin or similar.

func (ShellExecutor) ResolveHome

func (ShellExecutor) ResolveHome(ctx context.Context, user string) (string, error)

ResolveHome returns $HOME for `user` on the local host. Empty user resolves to the invoking operator's $HOME (matches today's `os.Getenv("HOME")` behaviour). Non-empty user goes through `getent passwd <user>` so callers can resolve any user's home.

func (ShellExecutor) RunBuilder

func (ShellExecutor) RunBuilder(ctx context.Context, opts spec.BuilderRunOpts) ([]byte, error)

RunBuilder delegates to the package-level BuilderRun.

func (ShellExecutor) RunCapture

func (ShellExecutor) RunCapture(ctx context.Context, script string) (string, string, int, error)

RunCapture executes a shell command on the local host and returns captured stdout/stderr/exit. Mirrors the deleted ContainerExecutor / ImageExecutor / VmTestExecutor behaviour from the pre-cutover test- time interface — callers (testrun.go verbs) get the same return shape via the unified DeployExecutor interface.

func (ShellExecutor) RunInteractive

func (ShellExecutor) RunInteractive(_ context.Context, script string) (int, error)

func (ShellExecutor) RunStream

func (ShellExecutor) RunStream(_ context.Context, script string) (int, error)

func (ShellExecutor) RunSystem

func (s ShellExecutor) RunSystem(ctx context.Context, script string, opts spec.EmitOpts) error

RunSystem delegates to the package-level runSudoShell.

func (ShellExecutor) RunUser

func (s ShellExecutor) RunUser(ctx context.Context, script string, opts spec.EmitOpts) error

RunUser delegates to the package-level runUserShell.

func (ShellExecutor) StartProcess

func (ShellExecutor) StartProcess(ctx context.Context, launch spec.ProcessLaunch) (spec.Process, error)

StartProcess launches argv directly on the local venue. No shell is involved.

func (ShellExecutor) Venue

func (ShellExecutor) Venue() string

Venue returns the fixed "local" identifier — commands always run on the invoking user's host.

Jump to

Keyboard shortcuts

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