Documentation
¶
Overview ¶
Package container implements the executor.Executor interface on top of the Docker/Podman command line, running each workload in a resource- and network-isolated sandbox.
It exists because pkg/executor/localprocess — the driver cloop ships with by default — offers no isolation at all: a task runs as the control-plane user, sees the whole host filesystem, and can reach anything on the network. That is defensible on a laptop and indefensible for a hosted deployment where the agent executes model-authored code on someone else's behalf.
What the sandbox actually guarantees ¶
Every container is started with:
- only the project directory bind-mounted, at a fixed path (ContainerWorkspace). Nothing else of the host is visible.
- a non-root UID derived from the project directory's owner, so files the workload creates are owned correctly on the host and a container escape lands on an unprivileged user. (Under rootless podman the same is achieved with --userns=keep-id.)
- --network=none by default. Egress is opt-in per executor, and when it is opted into it can be filtered at the IP layer — see Egress below.
- all Linux capabilities dropped and no-new-privileges set, so a setuid binary inside the image cannot undo the UID choice.
- --cpus / --memory / --pids-limit from the Spec's ResourceLimits, with swap pinned to the memory ceiling so paging cannot evade it.
The exact command line is built by buildRunArgs in argv.go, which is pure and exhaustively unit-tested. That file is the security boundary; this one is lifecycle plumbing around it.
Egress ¶
It used to say here that this driver does not filter egress, and that per-destination policy belonged to somebody else. That was true and it was a hole: an operator who opted into a network got unrestricted outbound access, and the egress broker's allowlist bound only a workload that chose to honour $HTTP_PROXY. A harness opening a raw socket ignored all of it.
firewall.go closes that. With executors.container.egress_filter enabled the executor provisions a network of its own and either makes it --internal — no route off the host, so the broker is the only way out — or installs an nftables ruleset on the sandbox bridge compiled by pkg/netfilter from the same authorisation the broker enforces. The filter is off by default, because switching it on under a running deployment would look like a network outage rather than a policy.
What it still cannot do is enforce a hostname allowlist, because hostnames do not exist at layer 3; that compiles to "the public Internet on these ports" and the compiled policy carries a warning saying so. Preflight reports both the filter's presence and its scope.
What it deliberately does not do ¶
It does not pull images. Start passes --pull=never so a cold image cache fails immediately with an actionable error instead of turning a UI click into a multi-minute hang. Preflight tells the operator what to pull.
Secrets ¶
Provider API keys and brokered secrets arrive in Spec.Env and are forwarded with the bare `--env NAME` form, so the runtime reads each value from its own environment and no secret ever appears in the host process table. Nothing is baked into an image and nothing is written to the mounted workdir.
One divergence from localprocess is deliberate: a nil Spec.Env means "no host environment at all" here, where os/exec semantics would mean "inherit everything". Forwarding the control plane's entire environment into a sandbox would hand the workload every credential the server holds, which is precisely what this driver exists to prevent. Callers pass what the workload needs, explicitly.
Index ¶
- Constants
- Variables
- func AuditBuildNetwork(opts Options, spec executor.Spec) (string, error)
- func AuditRunArgv(opts Options, rt AuditRuntime, workDir string, spec executor.Spec) ([]string, error)
- func AuditSecretStaging(opts Options, rt AuditRuntime, workDir string, spec executor.Spec) (argv []string, dirs []string, cleanup func(), err error)
- func ContainerName(workDir, runID string) string
- func IsVirtualizedOCIRuntime(name string) bool
- func ValidateExtraArgs(extra []string) error
- func ValidateImageRef(ref string) error
- func ValidateNetwork(name string) error
- func ValidateNonRootUser(user string) error
- func ValidateOCIRuntime(name string) error
- type AuditRuntime
- type EgressFilter
- type Executor
- func (e *Executor) Attach(ctx context.Context, req executor.AttachRequest) (executor.AttachConn, error)
- func (e *Executor) AttachHandleStore(store executor.HandleStore)
- func (e *Executor) Capabilities() executor.Capabilities
- func (e *Executor) HandleStatuses(ctx context.Context) ([]executor.Status, error)
- func (e *Executor) Handles() []string
- func (e *Executor) HealthCheck(ctx context.Context) error
- func (e *Executor) HoldsLease(leaseID string) bool
- func (e *Executor) ID() string
- func (e *Executor) Image() string
- func (e *Executor) KernelIsolated() bool
- func (e *Executor) Kind() string
- func (e *Executor) Leases() []string
- func (e *Executor) OCIRuntime() string
- func (e *Executor) Preflight(ctx context.Context, workDir string) PreflightReport
- func (e *Executor) ReapOrphans(ctx context.Context) ([]string, error)
- func (e *Executor) ResolveDigest(ctx context.Context, ref string) (string, error)
- func (e *Executor) ResolveImage(ctx context.Context, ref string) (string, error)
- func (e *Executor) Revocations() []executor.RevokeOutcome
- func (e *Executor) RevokeLease(ctx context.Context, req executor.RevokeRequest) executor.RevokeOutcome
- func (e *Executor) Runtime() Runtime
- func (e *Executor) Signal(ctx context.Context, handleID string, sig executor.Signal) error
- func (e *Executor) SmokeTest(ctx context.Context, workDir string) (SmokeTestResult, error)
- func (e *Executor) Start(ctx context.Context, spec executor.Spec) (executor.Handle, error)
- func (e *Executor) Status(ctx context.Context, handleID string) (executor.Status, error)
- func (e *Executor) Stream(ctx context.Context, handleID string) (<-chan executor.LogLine, error)
- func (e *Executor) SupportsRevocation() bool
- func (e *Executor) Virtualized() bool
- type Finding
- type ImageIdentity
- type Options
- type PreflightReport
- type Runtime
- type SmokeTestResult
Constants ¶
const ( // LabelManaged marks a container as created by cloop. LabelManaged = "cloop.managed" // LabelExecutor records which executor instance started it. LabelExecutor = "cloop.executor" // LabelHandle records the executor handle ID. LabelHandle = "cloop.handle" // LabelProject records the host project directory. LabelProject = "cloop.project" // LabelSandboxHash records the .cloop/sandbox.yaml content hash the // container was shaped by, so a running container can be tied back to the // spec that produced it even after the file has been edited. LabelSandboxHash = "cloop.sandbox" // LabelDevices records the names of the granted host devices exposed to // the container, so an operator looking at a running sandbox can see what // hardware it holds without reconstructing it from the lease. LabelDevices = "cloop.devices" // LabelEgressScope records the per-project egress confinement in force, // which is otherwise invisible: the network name alone does not say // whether the workload was cut off from private address space. LabelEgressScope = "cloop.egress_scope" )
Labels applied to every container this driver starts. They are what make orphan reaping possible: a control plane that was killed mid-run finds its leftovers by label, not by remembering them.
const ( // NetworkNone fully isolates the workload: no interfaces but loopback. NetworkNone = "none" // NetworkBridge attaches the runtime's default bridge, giving the // workload unrestricted outbound access. NetworkBridge = "bridge" )
Network modes.
const ( LabelDerivedFrom = "io.cloop.sandbox.base" LabelSetupHash = "io.cloop.sandbox.setup" )
LabelDerivedFrom and LabelSetupHash are stamped onto derived images so an operator running `podman images --filter label=...` can tell what a cloop-sandbox tag was built from, and `podman image prune` decisions are informed rather than guessed.
const ( // LevelOK: the check passed. LevelOK = "ok" // LevelWarn: usable, but with a caveat the operator should know about. LevelWarn = "warn" // LevelFail: workloads will not run until this is fixed. LevelFail = "fail" )
Preflight check levels.
const ( RuntimePodman = "podman" RuntimeDocker = "docker" )
Supported container runtime names. Both speak a compatible enough CLI that one argv builder covers them; anything else is rejected rather than hopefully attempted, because the argv *is* the security boundary and we cannot reason about flags a third runtime may interpret differently.
const ContainerCloopPath = "/usr/local/bin/cloop"
ContainerCloopPath is where the cloop binary is expected inside the sandbox, and where the smoke test bind-mounts the control plane's own binary so `cloop executor test` works against any base image.
const ContainerWorkspace = "/workspace"
ContainerWorkspace is the fixed in-container path the project directory is bind-mounted at. Fixed rather than mirroring the host path so that nothing inside the sandbox learns the host's directory layout, and so a workload's behaviour does not change when the same project is checked out elsewhere.
const DefaultID = "container"
DefaultID is the executor ID used when none is configured.
const DefaultImage = "ghcr.io/blechschmidt/cloop-harness:latest"
DefaultImage is the reference used when executors.container.image is unset.
The image is a contract, not just a base OS. A sandbox image must provide:
- the cloop binary at /usr/local/bin/cloop (the harness re-invokes cloop subcommands from inside the sandbox);
- whichever agent harness the project's provider needs (for the default claudecode provider, the `claude` CLI on PATH);
- git, plus a CA bundle, since almost every task shells out to one or the other;
- a non-root user, because the driver runs as the project directory's owner UID and that UID must be able to resolve inside the container.
It deliberately does *not* bake in credentials: provider API keys and scoped secrets are injected as environment at start (see buildRunArgs), so the same image is safe to share across tenants and to publish.
Until a published image exists this reference will not resolve, which is the correct failure: Preflight reports "image not present" with the pull command rather than silently falling back to something unaudited. Operators running an image of their own set executors.container.image.
const ( // DefaultOrphanGracePeriod protects a *running* container young enough // that it might belong to a dispatch still in flight. Only running // containers older than this are collected by ReapOrphans; exited ones are // collected immediately and ignore it entirely. // // The value is kubernetes.DefaultOrphanGracePeriod, deliberately: the // window it covers is the same window (a Start between creating the // workload and tracking it, or a peer control plane booting against the // same backend), and an operator who has reasoned about one driver's sweep // should not find the other one behaves differently. Ten minutes is far // wider than the millisecond-scale race it guards, which is the correct // direction to be wrong in: an orphan reaped ten minutes late costs CPU, // an orphan reaped ten milliseconds early costs somebody's run. DefaultOrphanGracePeriod = 10 * time.Minute )
const DerivedImagePrefix = "localhost/cloop-sandbox"
DerivedImagePrefix is the repository derived sandbox images are tagged under. "localhost/" keeps podman from treating the name as a Docker Hub reference and trying to pull it.
const FileNameHint = ".cloop/sandbox.yaml"
FileNameHint names the file a build failure came from. It is a string here rather than an import of pkg/sandbox because that package imports pkg/config, which imports this one.
const KVMDevice = "/dev/kvm"
KVMDevice is the host device Kata needs in order to start a VM. Its absence is the single most common reason a kata executor fails on a cloud host: the hub is itself a VM and nested virtualization is off.
const LabelInterfaces = "cloop.interfaces"
LabelInterfaces records the grant handles of the host interfaces moved into the container, so an operator looking at a running sandbox can see which segments it holds without reconstructing them from the lease.
const ReapedRunningSuffix = " (running)"
ReapedRunningSuffix marks an entry in ReapOrphans' result as a container that was still running when it was collected, rather than one that had already exited.
The distinction has to travel in the string because the result is a []string the caller renders more or less verbatim, and the two cases mean very different things to whoever reads that line: an exited orphan was costing disk, a running one was burning a core with nobody reading its output — and, unlike the exited case, collecting it destroyed work in progress. A caller that wants to say so can test for this suffix.
Variables ¶
var ErrNoRuntime = errors.New("container: no container runtime found")
ErrNoRuntime is returned when no supported container runtime is installed. Tests use errors.Is against it to skip cleanly.
Functions ¶
func AuditBuildNetwork ¶
AuditBuildNetwork returns the network a derived-image build would run with for spec.
It is exposed for the same reason AuditRunArgv is. `setup:` executes repo-authored commands, so the build's network posture is a security boundary — a build with unconditional egress would let a pull request reach the Internet from a deployment configured to forbid it. That posture is decided inside ensureDerivedImage, behind a real image store and a real builder, and a property only reachable on a machine with podman installed is a property CI does not check.
func AuditRunArgv ¶
func AuditRunArgv(opts Options, rt AuditRuntime, workDir string, spec executor.Spec) ([]string, error)
AuditRunArgv returns the exact command line the container executor would hand to the runtime for spec in workDir.
workDir must exist: the driver stats it to derive the UID mapping, and a fabricated path would silently take a different branch than production.
func AuditSecretStaging ¶
func AuditSecretStaging(opts Options, rt AuditRuntime, workDir string, spec executor.Spec) (argv []string, dirs []string, cleanup func(), err error)
AuditSecretStaging stages spec.SecretFiles the way Start does and returns the resulting command line together with the host directories the credentials were written into.
It exists because the property worth asserting is not "the driver has a function that writes files" but "the bytes a lease produced end up somewhere the sandbox can read and nowhere else" — the mode they are created with, the directory's mode, and the :ro on the bind that carries them. All three are decided inside Start, past a runtime the conformance suite cannot assume is installed, so without this seam the delivery that Task 20192 exists to build would be exactly as untested as the drop it replaced.
The caller must invoke cleanup, which wipes the staged credentials. A test that skips it leaves plaintext in /dev/shm.
func ContainerName ¶
ContainerName derives the deterministic container name for a run: cloop-<projectSlug>-<runID>.
Deterministic naming is what makes orphans reapable. A control plane killed mid-run leaves containers behind with no in-memory record; `cloop executor reap` finds them by the cloop.managed label, and an operator reading `podman ps` can tell at a glance which project a container belongs to.
func IsVirtualizedOCIRuntime ¶
IsVirtualizedOCIRuntime reports whether name identifies a runtime that puts the workload behind a hypervisor rather than on the host kernel.
It is executor.IsVirtualizedRuntime under this package's vocabulary. The matcher is shared with the Kubernetes driver, which asks the same question about a RuntimeClass name: see pkg/executor/virtualization.go for why one definition rather than two, and why it recognises so little.
func ValidateExtraArgs ¶
ValidateExtraArgs checks operator-supplied runtime flags.
Two rules, both necessary:
Every argument must begin with "-". Without this a stray positional would be consumed as the image reference, silently promoting the real image to the command position and running an operator-chosen image.
Flags taking a value must use --flag=value. The alternative (--flag value) is indistinguishable from a positional here, because arity is a property of the runtime's own flag table that we do not have.
Exported so `cloop config set` and the config validator can reject a bad value at the point the operator types it, rather than at the next run.
func ValidateImageRef ¶
ValidateImageRef rejects references that could be re-read as flags or that smuggle shell metacharacters. It is deliberately not a full grammar for OCI references: the goal is to keep the *argv* unambiguous, and the runtime does the authoritative parsing.
func ValidateNetwork ¶
ValidateNetwork allows the two well-known modes plus operator-defined network names. It rejects host networking outright: --network=host removes the network namespace entirely, so the workload can reach every service bound to the host's loopback — including the control plane's own API and any unauthenticated metadata endpoint.
The rule itself lives in executor.ValidateNetworkName, which the per-executor sandbox settings also use. Two copies would be two places for "host" to stop being refused, and only one of them would be the one an admin's HTTP request passed through.
func ValidateNonRootUser ¶
ValidateNonRootUser rejects a --user value that would run the workload as UID 0 inside the container, or leave the user unset.
Root in a container is not the same as root on the host, but it is one kernel bug or one careless bind mount away from being exactly that, and it makes --cap-drop=ALL far less valuable than it looks. An unset user is worse than an explicit root: most base images default to root, so "no --user flag" is root without saying so.
The refusal is deliberate rather than a silent downgrade to a fixed UID like nobody. The workspace is a bind mount from the host, so a UID the operator did not choose produces a container that cannot write to its own project directory — a confusing mid-run permission failure instead of an immediate, actionable one.
func ValidateOCIRuntime ¶
ValidateOCIRuntime checks that name is a plausible OCI runtime *name*.
Empty is valid and means "the runtime's own default" — no --runtime flag is emitted at all, which is what every existing deployment gets.
The rule itself lives in executor.ValidateRuntimeName, which is also what the per-executor sandbox settings an admin sets in the UI are checked against. One definition rather than two is the point: a name the hub's config.yaml accepts and its API refuses — or the reverse — would be a difference of opinion about the same `--runtime` value, and the more permissive of the two would be the one that decided.
Types ¶
type AuditRuntime ¶
AuditRuntime describes the runtime to build argv for, without probing the host. Rootless selects podman's keep-id UID mapping; rootful takes the explicit --user path.
type EgressFilter ¶
type EgressFilter struct {
// Enabled turns the filter on. Without it the remaining fields are
// inert and the driver reports no filtering.
Enabled bool
// Internal puts the sandbox on a runtime network with no route off the
// host, making the broker the only path out. It is the recommended
// setting and needs no host privileges.
Internal bool
// AllowCIDRs, AllowPorts, AllowPublicInternet and Resolvers describe
// direct egress, compiled by pkg/netfilter into nftables rules on the
// sandbox bridge. Naming any of them requires nft(8) and CAP_NET_ADMIN
// on the host.
AllowCIDRs []string
AllowPorts []int
AllowPublicInternet bool
Resolvers []string
// AllowAllPorts waives the port restriction that accompanies a
// destination allow. It is not operator-settable — pkg/config has no key
// for it — and exists for the per-project scopes in effectiveFilter,
// where the policy's bound is the block set rather than a port list.
//
// Keeping it off the config surface is deliberate. An operator naming
// allow_cidrs with no ports has described a hole, and netfilter.Compile
// refuses that on purpose; this field would turn that refusal into a
// footgun with a yaml key.
AllowAllPorts bool
// Broker is the egress proxy endpoint ("10.7.0.2:8118") the sandbox
// must reach. Only needed alongside a direct-egress filter; on an
// --internal network the broker is reachable because it shares the
// bridge.
Broker string
// HostPatterns carries the L7 allowlist purely so the compiled policy
// can warn that it is not enforcing it.
HostPatterns []string
}
EgressFilter is the operator's declaration of what a sandbox on this executor may reach.
The zero value means "no filter", which preserves the behaviour of every deployment predating this type: the configured Network is used as-is. That default is deliberate. Silently firewalling a running deployment on upgrade would break it in a way that looks like a network outage, so switching this on is something an operator does.
func (EgressFilter) Compile ¶ added in v0.0.2
func (f EgressFilter) Compile() (netfilter.Policy, error)
Compile turns the filter into the policy the nftables renderer consumes, whether or not Enabled is set.
The Enabled-independence is the point, and it mirrors kubernetes.EgressFilter.Compile. pkg/config compiles at load so that a CIDR with a typo in it is a startup error naming the key; if that check skipped switched-off sections, the typo would sit in the file until the day an operator flipped the boolean, and would surface then as a sandbox whose every network request fails — the furthest possible point from the edit that caused it.
Errors name the YAML key and the offending value, unprefixed, because the operator reading one is looking at their config file. Callers applying a filter rather than checking one (Policy, Validate) add the driver context.
func (EgressFilter) Policy ¶
func (f EgressFilter) Policy() (netfilter.Policy, error)
Policy compiles the filter for use, refusing a filter that is switched off.
It is exported and pure so that `cloop egress firewall` can render what a given configuration would install without touching the host. Callers that want to *validate* an authorisation rather than apply it want Compile, which does not care whether the switch is on.
func (EgressFilter) Validate ¶
func (f EgressFilter) Validate() error
Validate checks the filter without touching the host.
A switched-off filter is accepted unconditionally: this runs when the driver builds an executor, and refusing to register one over a typo in an inert section would turn a latent misconfiguration into an outage. Reporting that typo is pkg/config's job, where the operator is looking at the file.
type Executor ¶
type Executor struct {
// contains filtered or unexported fields
}
Executor runs workloads in containers. Safe for concurrent use.
func Ensure ¶
Ensure registers a container executor built from opts into reg (nil means executor.DefaultRegistry), unless one with the same ID is already present.
It returns the executor so callers can preflight it. A missing runtime is reported as an error rather than swallowed: an operator who configured a container executor and silently got host execution would believe they had an isolation boundary they do not have.
func New ¶
New returns a container executor, detecting the runtime described by opts. It performs no network or filesystem I/O beyond resolving the runtime binary on PATH, so constructing an executor on a host without a runtime fails fast and legibly rather than at the first run.
func (*Executor) Attach ¶ added in v0.0.2
func (e *Executor) Attach(ctx context.Context, req executor.AttachRequest) (executor.AttachConn, error)
Attach implements executor.Attacher.
func (*Executor) AttachHandleStore ¶
func (e *Executor) AttachHandleStore(store executor.HandleStore)
AttachHandleStore installs the durable handle store after construction and immediately reattaches to whatever it describes.
It exists because of a construction-order problem with no tidy fix: the executor registry is built from configuration, early and synchronously, while the state database that backs the store is opened later by the process that needs it. Requiring the store at New would mean either opening the database before the config that names it has been validated, or leaving every executor that is legitimately storeless — `cloop executor test`, Preflight, the config validator — unable to construct at all.
Calling it more than once is safe, including with a different store: see rehydrate for why re-adoption cannot produce a duplicate pump.
func (*Executor) Capabilities ¶
func (e *Executor) Capabilities() executor.Capabilities
Capabilities implements executor.Executor.
SharesHostFilesystem is true because the project directory really is a bind mount of a host path — host-side tooling can read what the workload wrote. NetworkEgress reflects the configured network honestly: claiming isolation the configuration does not provide is the failure mode that matters.
SupportsWorkspaceProvisioning is false, and that is the answer rather than a gap. See buildRequest: this driver's whole model is that WorkDir is the operator's own checkout, mounted in. There is no tree for it to fetch.
Isolation is IsolationVM rather than IsolationContainer when the executor is configured with a Kata OCI runtime, because that is literally what the enum means: the workload runs in a VM with a kernel of its own. The claim is made from the configured runtime name and nothing else — whether kata can actually start is Preflight's question, and answering it here would mean probing the host on every call to a method callers treat as free.
func (*Executor) HandleStatuses ¶
HandleStatuses implements executor.Lister from the driver's own bookkeeping. It deliberately does not shell out to `podman ps`: the Executors panel reads this for every card it renders, and a wedged runtime socket would turn a status column into a page-load stall. Containers this process did not start are not ours to report on anyway.
func (*Executor) Handles ¶
Handles returns the IDs of every handle this executor still knows about, newest first.
func (*Executor) HealthCheck ¶
HealthCheck implements executor.Executor. It verifies the runtime binary still responds; a full environment audit is Preflight's job.
func (*Executor) HoldsLease ¶
HoldsLease reports whether any container this executor tracks was started with material from leaseID.
func (*Executor) KernelIsolated ¶ added in v0.0.2
KernelIsolated reports whether the workload's system calls are served by something other than the host kernel — a Kata guest kernel or gVisor's Sentry.
Isolation stays IsolationContainer for gVisor even when this is true, and the two statements do not conflict. gVisor is a container by every structural measure the enum describes: namespaces, a cgroup, an image, no VM. What it changes is who executes the syscalls, which is a different axis and gets a different field rather than a fourth enum value that nothing else understands.
func (*Executor) OCIRuntime ¶
OCIRuntime reports the configured low-level runtime, empty when the CLI's default is in use.
func (*Executor) Preflight ¶
func (e *Executor) Preflight(ctx context.Context, workDir string) PreflightReport
Preflight verifies that this executor can actually run a workload in workDir. Passing an empty workDir skips the filesystem checks and reports only on the runtime and image.
It never returns an error for a *failed check* — those are findings. Callers use report.Err() when they want a single error, and the findings themselves when they want to show a checklist.
func (*Executor) ReapOrphans ¶
ReapOrphans removes containers this control plane created but no longer tracks — the residue of a control plane that was killed mid-run.
It collects two populations, and they are deliberately not symmetric.
Exited containers are removed immediately and across every executor id, exactly as they always have been. An exited container holds a name and a writable layer and nothing else, so the worst case of removing one that belongs to a peer control plane is that peer losing a `docker logs` it had not read yet.
Running containers are the case that actually costs something — before Task 20191 they were never touched at all, so a hub killed mid-run left a sandbox burning CPU forever, with nobody reading its output and no handle able to stop it — and they are also the case where a wrong answer destroys work in progress. So a running container is collected only when all of the following hold: it carries this executor's own id (two container executors on one host must not reap each other's work), it carries a handle label (a hand-made container wearing cloop.managed=true is not ours to kill), this executor does not track it, and the *runtime* says it has been running for longer than OrphanGracePeriod. See shouldReapRunningOrphan.
An executor with a HandleStore adopts its own containers at construction and therefore tracks them, so this sweep only ever sees containers left by a process that had no durable store or whose rows were lost. That is the intended relationship between the two halves of Task 20191: rehydration saves the run, and reaping is what happens when it could not be saved.
It returns the names of the containers removed, the running ones carrying ReapedRunningSuffix.
func (*Executor) ResolveDigest ¶
ResolveDigest implements imagepolicy.Resolver against the local image store.
It reports imagepolicy.ErrNoDigest rather than an error when the image is present but carries no repo digest, which is the case for anything built locally or loaded from a tarball. The two outcomes need opposite handling — one means "nothing to pin to", the other means "the image is not here" — and collapsing them would make a missing image look like an unsigned one.
func (*Executor) ResolveImage ¶
ResolveImage implements executor.ImageResolver: it returns ref pinned to a digest where one is available.
func (*Executor) Revocations ¶
func (e *Executor) Revocations() []executor.RevokeOutcome
Revocations reports this executor's revocation log for the Secrets panel.
func (*Executor) RevokeLease ¶
func (e *Executor) RevokeLease(ctx context.Context, req executor.RevokeRequest) executor.RevokeOutcome
RevokeLease takes one lease's material back from every container holding it.
It returns an outcome rather than an error for the reason the remote driver documents: the caller's next move depends on which failure, and an error alone cannot say whether the material is gone, in doubt, or still out there. The log entry is written before any wiping starts, so a revocation is never lost to a crash between the two.
func (*Executor) Signal ¶
Signal implements executor.Executor by asking the runtime to deliver the signal, rather than signalling a host PID: the container's PID namespace means the host PID and the in-container PID 1 are different numbers, and only the runtime knows the mapping.
func (*Executor) SmokeTest ¶
SmokeTest runs `cloop version` inside the sandbox and returns what it printed.
workDir is the project directory to bind-mount; empty uses a throwaway temporary directory, which is the right choice for testing the executor itself rather than a particular project.
The control plane's own binary is mounted read-only at ContainerCloopPath so the test works against images that do not yet ship cloop. It is mounted read-only specifically so a compromised sandbox cannot rewrite the binary that the host will execute next.
func (*Executor) Start ¶
Start implements executor.Executor: it launches a detached container and returns its handle. The container outlives ctx, which bounds only the act of starting.
func (*Executor) SupportsRevocation ¶
SupportsRevocation reports whether a revocation issued now would be honoured.
Always true for this driver. Unlike a remote agent — which can be offline, or too old to understand the frame — the sandbox runs on this host, its credential files are in a directory this process created, and the runtime that would have to kill it is the one already being used to start it. There is no third party whose availability the guarantee depends on.
func (*Executor) Virtualized ¶
Virtualized reports whether workloads run behind a hypervisor.
type Finding ¶
type Finding struct {
// Name identifies the check, e.g. "runtime", "image", "workdir".
Name string `json:"name"`
// Level is one of LevelOK, LevelWarn, LevelFail.
Level string `json:"level"`
// Message states what was observed.
Message string `json:"message"`
// Fix is the concrete remedy, empty when Level is LevelOK.
Fix string `json:"fix,omitempty"`
}
Finding is one preflight observation.
type ImageIdentity ¶
type ImageIdentity struct {
// Ref is the reference as requested.
Ref string
// RepoDigest is the registry-reproducible "repo@sha256:..." form, empty
// when the image has none — true for locally-built images, and for images
// loaded from a tarball.
RepoDigest string
// ID is the local content ID ("sha256:..."), always present for an image
// that exists locally.
ID string
}
ImageIdentity is what a reference resolved to locally.
func InspectImage ¶
InspectImage resolves ref against the local image store.
A missing image is an error naming the pull command, because the driver never pulls implicitly: an implicit pull turns a cold start into an unbounded network wait inside an HTTP handler, and turns a typo'd sandbox.yaml into a registry request from the hub.
func (ImageIdentity) Pinned ¶
func (i ImageIdentity) Pinned() string
Pinned returns the strongest reference that still resolves to this exact image: the repo digest when there is one, otherwise the reference as given.
It deliberately does not fall back to the bare image ID. Running by ID works, but it produces a container whose image field is a hash with no repository, which makes every downstream artifact — logs, `podman ps`, the run record — unable to say what was executed. A tag that could move is a worse pin but a better record, and the artifact stores the ID alongside it either way.
type Options ¶
type Options struct {
// ID is the executor's registry identifier. Empty means DefaultID.
ID string
// Runtime pins the container runtime ("podman" or "docker"). Empty
// auto-detects, preferring rootless podman.
Runtime string
// OCIRuntime pins the low-level runtime that Runtime delegates to
// ("kata", "kata-qemu", "crun", ...). Empty leaves the CLI's default,
// which is what every deployment predating this field gets.
//
// A Kata name here is what makes this executor a VM sandbox rather than a
// container one, and Capabilities says so. See ociruntime.go for why it
// must be a registered name and never a path.
OCIRuntime string
// Image is the sandbox image reference. Empty means DefaultImage.
Image string
// CPUs is the default core allowance when a Spec requests none.
CPUs float64
// MemoryMB is the default memory ceiling when a Spec requests none.
MemoryMB int
// PIDsLimit is the default process cap. Zero means defaultPIDsLimit;
// negative means explicitly unlimited.
PIDsLimit int
// Network is "none" (default), "bridge", or an operator-defined network.
Network string
// AllowHosts pins name resolution as "host:address" entries. Only
// meaningful when Network is not "none".
AllowHosts []string
// ExtraArgs are additional runtime flags. Validated by ValidateExtraArgs.
ExtraArgs []string
// SELinuxLabel is "", "z", or "Z" — the relabel option applied to bind
// mounts on SELinux hosts.
SELinuxLabel string
// AllowRootUser permits the workload to run as uid 0 inside the
// container. Default false, and it should stay false.
//
// The UID is normally derived from the project directory's owner, so a
// control plane running as root over a root-owned project silently
// produces a root sandbox — capabilities dropped, but still uid 0 against
// a bind mount from the host and the full kernel surface. That is a
// decision an operator should make on purpose, which is what this flag
// makes it: without it, such a configuration is refused with an error
// that says how to fix it.
AllowRootUser bool
// EgressFilter is the IP-layer egress policy applied to every sandbox
// this executor starts. The zero value filters nothing, which is what
// every deployment predating it gets. See firewall.go.
//
// When it is enabled the executor stops using Network above and runs
// sandboxes on a network of its own, because the filter and the network
// are one decision: an operator-named network could be shared with
// workloads this executor does not manage, and installing a default-deny
// ruleset on their bridge would firewall them too.
EgressFilter EgressFilter
// HandleStore persists handle identity so this executor can reattach to
// its own containers after the control plane restarts (Task 20191). See
// rehydrate.go for what reattachment can and cannot rebuild.
//
// Nil means no persistence, which is exactly the pre-Task-20191 behaviour
// and remains a supported configuration: the driver starts, streams,
// signals and reaps as it always did. What it loses is survival. A hub
// killed mid-run comes back with an empty handle map, so Stream, Status
// and Signal answer ErrHandleNotFound for containers that are still alive,
// and the only thing that eventually reclaims them is the running half of
// ReapOrphans — after OrphanGracePeriod, and by killing the work.
//
// It is a field rather than a constructor argument because a container
// executor is routinely built before there is anything durable to give it:
// `cloop executor test`, Preflight and the config validator all construct
// a driver with no state database in sight. Those callers pass nil and get
// a working executor; the control plane calls AttachHandleStore once the
// database is open.
HandleStore executor.HandleStore
// OrphanGracePeriod protects a running container young enough that it
// might belong to a Start still in flight, or to a second control plane
// that has just booted against the same runtime. Zero means
// DefaultOrphanGracePeriod; see shouldReapRunningOrphan for why the window
// is a correctness condition rather than a courtesy.
OrphanGracePeriod time.Duration
// ImagePolicy constrains the images a *project* may name in its
// .cloop/sandbox.yaml (Task 20177). The zero value constrains nothing.
//
// It deliberately does not apply to Image above. That reference is the
// operator's own choice, made in the same file as the policy; running it
// through an allowlist the same person wrote would be a lint, not a
// control, and with a safe chart default of "digests only" it would refuse
// the hub's own tagged image at boot.
ImagePolicy imagepolicy.Policy
}
Options configures a container executor instance.
The zero value is usable: it auto-detects a runtime, uses DefaultImage, disables networking, and applies defaultPIDsLimit. Every field maps to an executors.container.* config key.
type PreflightReport ¶
type PreflightReport struct {
ExecutorID string `json:"executor_id"`
Runtime string `json:"runtime"`
Image string `json:"image"`
WorkDir string `json:"work_dir,omitempty"`
Findings []Finding `json:"findings"`
}
PreflightReport is the result of checking an executor's environment.
func (PreflightReport) Err ¶
func (r PreflightReport) Err() error
Err returns an error summarising the fatal findings, or nil when none.
func (PreflightReport) OK ¶
func (r PreflightReport) OK() bool
OK reports whether nothing fatal was found.
type Runtime ¶
type Runtime struct {
// Name is one of the Runtime* constants.
Name string
// Path is the absolute path to the binary, resolved once at detection
// so a later PATH change cannot swap the binary out from under a run.
Path string
// Rootless reports whether workloads will run in a user namespace owned
// by an unprivileged host user. It changes the UID mapping strategy:
// see buildRunArgs.
Rootless bool
}
Runtime is a resolved container runtime binary.
func DetectRuntime ¶
DetectRuntime resolves the container runtime to use.
preferred may be a bare name ("podman", "docker") or an absolute path to one of those binaries. An empty preferred auto-detects in supportedRuntimes order. A name outside the allowlist is an error: config must not be able to point cloop at an arbitrary executable, since that executable is invoked with the workload's environment (which holds the caller's secrets).
type SmokeTestResult ¶
type SmokeTestResult struct {
// Image is the sandbox image the test ran in.
Image string `json:"image"`
// Runtime describes the container runtime used.
Runtime string `json:"runtime"`
// OCIRuntime is the low-level runtime the CLI delegated to, empty when
// the CLI's default was used. When this names a Kata runtime, a
// successful smoke test is the proof that a VM actually booted — which
// no amount of preflight can establish.
OCIRuntime string `json:"oci_runtime,omitempty"`
// ContainerName is the deterministic name that was used, so an operator
// can correlate with runtime logs.
ContainerName string `json:"container_name"`
// Output is the workload's combined stdout+stderr, trimmed.
Output string `json:"output"`
// ExitCode is the workload's exit status.
ExitCode int `json:"exit_code"`
// Duration is the wall-clock time from start to reap.
Duration time.Duration `json:"duration"`
// MountedBinary is the host path bind-mounted at ContainerCloopPath,
// empty when the image's own cloop was used.
MountedBinary string `json:"mounted_binary,omitempty"`
}
SmokeTestResult is the outcome of a sandbox smoke test.