container

package
v0.2026269.1703 Latest Latest
Warning

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

Go to latest
Published: Sep 26, 2026 License: MIT Imports: 16 Imported by: 0

Documentation

Overview

Package container is the spec fabric slice for container-engine host helpers — resolving which engine binary to invoke, its GPU run-args, auto-detecting the installed engine, and the engine capability data table. RELOCATED from sdk/kit (#55 fabric-primitive extraction). It carries os/exec (the engine auto-detect shells `LookPath`) in its OWN slice (Rule 2) so a consumer needing only value types never drags os/exec. charly core inlines from here; sdk/kit re-exports the same symbols so existing kit.EngineBinary / kit.GPURunArgs / kit.DetectEngine call sites are untouched.

The engine vocabularies are CUE-owned (schema/engine.cue #EngineName / #EngineRunMode / #EngineUnitRunModes, emitted as spec.EngineNames / spec.EngineRunModes / spec.EngineUnitRunModeWords) — this file declares no separate word LIST. Each predicate reads its OWN emitted list: IsEngineName → spec.EngineNames, IsRunMode → spec.EngineRunModes, IsUnitRunMode → spec.EngineUnitRunModeWords. The capability FACTS live in one Go table whose KEYS are the engine words (they appear as data, not as a second list); adding an engine is one CUE edit plus one table row, gated by TestEngineVocabularyIsSingleSource.

spec/container/transfer.go — the container-engine image-transfer host primitive, RELOCATED from sdk/kit by #55 coneC (charly/ off sdk/kit): it pipes an image from one engine's local store to another via `save | load` (os/exec), the fabric twin of the LocalImageExists probe that already lives in this slice. sdk/kit re-exports it (var TransferImage = container.TransferImage) so every existing kit.TransferImage call site (candy/plugin-build, candy/plugin-box, candy/plugin-deploy-pod, …) is unchanged; charly core inlines from here. Co-located with EngineBinary (same package) and the LocalImageExists family this transfer path complements.

Index

Constants

This section is empty.

Variables

View Source
var InspectLabels = InspectImageLabels

InspectLabels reads OCI labels from a local image via engine inspect. Package-level var for testability — defaults to the canonical InspectImageLabels (spec/container/image_inspect.go, coneA). Override this var (not kit/deploykit's re-exports) to stub label reads in tests of ExtractMetadata's decode logic.

View Source
var ListLocalImages = cachedListLocalImages

ListLocalImages returns all images in the engine's local storage, CACHED persistently (the image list does not change often — only a build or pull mutates it). The first call after the TTL expires re-fetches with user feedback; every subsequent call within the TTL reads the cache, so `charly status` is sub-second after the first run. Package-level var for testability (same pattern as LocalImageExists, DetectEngine).

View Source
var LocalImageExists = defaultLocalImageExists

LocalImageExists checks whether an image reference exists in the given engine's local store. Package-level var for testability (same pattern as DetectEngine). Co-located with the resolution family because ResolveLocalImageRef reads it; kit re-exports it (kit.LocalImageExists = container.LocalImageExists) so existing direct callers (candy/plugin-build, candy/plugin-box, candy/plugin-deploy-pod, candy/plugin-kube, charly core) are unchanged.

Functions

func ContainerImage

func ContainerImage(engine, containerName string) string

ContainerImage returns the image ref for a running container, best-effort ("" on error). Thin wrapper over ContainerImageRef.

func ContainerImageRef

func ContainerImageRef(engine, containerName string) (string, error)

ContainerImageRef returns the image ref backing a running container (.Config.Image via `<engine> inspect`). THE single container→image-ref inspector — there is exactly one inspect implementation.

func DetectEngine

func DetectEngine() (string, error)

DetectEngine auto-detects the container engine: prefers podman, falls back to docker. nerdctl is deliberately NOT auto-detected — it is opt-in per deploy/box via the `engine:` field, so an existing podman/docker install is never silently switched.

func DirectRunMode added in v0.2026265.2315

func DirectRunMode() string

DirectRunMode returns the non-unit run mode — the one member of the CUE-owned spec.EngineRunModes that is NOT unit-supervised. Derived as the set difference (EngineRunModes − EngineUnitRunModeWords), so the name has NO literal home: changing #EngineRunMode is the only edit. Returns "" if the vocabulary is malformed (no non-unit mode); TestDirectRunModeDerived pins the real vocabulary has exactly one.

func EngineBinary

func EngineBinary(engine string) string

EngineBinary returns the CLI binary name for a container engine. "auto" resolves via DetectEngine (podman preferred); an unknown/empty word falls back to the ONE default engine (spec.DefaultContainerEngine, podman) so a caller that names no engine gets the same engine the exec hop and DetectEngine use. A caller can distinguish the fallback from a real word via IsEngineName.

The behavior is served by the engine provider CLASS (the `binary` op); this predicate is its typed accessor, so the class has an in-tree consumer and the compiled-in / out-of-process placements answer identically.

func EngineCapabilityFor added in v0.2026265.2315

func EngineCapabilityFor(engine string) (spec.EngineCapability, bool)

EngineCapabilityFor returns the capability facts for an engine. There is NO distinct "unknown engine": the empty word AND any word that is not a member of the closed engine vocabulary both mean "the default engine" (spec.DefaultContainerEngine) — an unrecognized word can only be a typo or a pre-resolution artifact, and treating it as a separate engine would split one input across two engines' facts (podman's binary with docker's gpu args/probe/ mode, the regression this resolution closes). "auto" resolves through DetectEngine first; the bool is false ONLY when that detection fails.

A caller that needs to know whether a word is a REAL engine uses IsEngineName, not this bool — ValidateEngine is the authoring-time typo gate.

func EngineNames added in v0.2026265.2315

func EngineNames() []string

EngineNames returns the closed engine word vocabulary (podman/docker/nerdctl), sourced from the CUE-owned schema (schema/engine.cue #EngineName → spec.EngineNames) so no consumer holds a hand-written literal list.

func EngineOps added in v0.2026265.2315

func EngineOps() []string

EngineOps returns the op selectors the engine class serves. A provider advertises exactly these; a test asserts each is answered by InvokeEngineOp, so a newly-declared op cannot ship unserved.

func EngineRunModeFor added in v0.2026265.2315

func EngineRunModeFor(engine string) string

EngineRunModeFor returns the persistence/supervision mode for an engine word. An EMPTY word AND an unknown non-empty word both resolve to the ONE default engine (spec.DefaultContainerEngine) and return its mode — the same word EngineBinary resolves them to — so no input has anything but a single (binary, mode) resolution. The DirectRunMode() fallback is reached ONLY for "auto" when no engine is installed (the conservative, no-unit degraded path).

func EngineValidationError added in v0.2026265.2315

func EngineValidationError(field, value string) error

EngineValidationError builds the canonical "not an engine" error text from the CUE-owned vocabulary, so the message can never drift from the accepted set.

func ExtractCalVerTag

func ExtractCalVerTag(ref string) string

ExtractCalVerTag returns the CalVer portion of a ref's tag, or "" if the tag is not a recognisable CalVer (`YYYY.DDD.HHMM`). Lets the resolver distinguish CalVer tags from legacy floats like `:latest` (which should never be chosen as the newest candidate).

func ExtractMetadata

func ExtractMetadata(engine, imageRef string) (*spec.BoxMetadata, error)

ExtractMetadata reads OCI labels from a local image and returns parsed spec.BoxMetadata. Returns nil if the image has no ai.opencharly labels. Returns spec.ErrImageNotLocal wrapped with the image ref if the image is not in local storage.

func GPURunArgs

func GPURunArgs(engine string) []string

GPURunArgs returns the engine-specific run flags that expose all host GPUs to a container. podman uses the CDI device form; docker and nerdctl use the Docker-compatible `--gpus`. The style rides on the capability table so it is a fact, not a switch. The `"auto"` selector resolves to the installed engine via DetectEngine (the same resolution EngineBinary performs), so GPURunArgs("auto") matches GPURunArgs(<detected>).

Served by the class's `gpu_args` op; this is its typed accessor. The flag form is defined once, in engineGPUArgsRaw (the op body's leaf). The marshal path cannot fail for a plain string slice, but if it ever did this falls back to that SAME leaf — never a second copy of the flag form.

func InspectImageLabels

func InspectImageLabels(engine, imageRef string) (map[string]string, error)

InspectImageLabels reads a local image's OCI labels via engine inspect. Pure container-storage probe: no charly-core coupling. CACHED persistently — the labels are image METADATA (they do not change unless the image is rebuilt), so the first call after the TTL expires re-fetches and every subsequent call within the TTL reads the cache. The LIVE container state (podman ps) is never cached.

func InvalidateImageCache added in v0.2026241.1322

func InvalidateImageCache()

InvalidateImageCache clears the persistent image-list cache. Called by the build and deploy commands (charly box build / deploy add / update) — every operation that creates or pulls an image — so the next status run re-fetches the fresh image list instead of serving a stale cache.

func InvokeEngineOp added in v0.2026265.2315

func InvokeEngineOp(reserved, op string, params json.RawMessage) (json.RawMessage, error)

InvokeEngineOp dispatches one engine-class op for the provider word `reserved` (podman/docker/nerdctl) and returns the reply as JSON. It is the placement- agnostic body both a compiled-in charly provider and an out-of-process plugin wrap. An unknown op is a hard error (never a silent empty reply), so a dispatch gap is visible.

`params` is the raw spec.Engine*Request JSON the caller marshalled; the resolved engine word rides INSIDE the request (the envelopes carry an `engine`/no field), so `reserved` is only the fallback when the request omits it — a caller may invoke any engine word through any engine provider (the provider is a servant of the CLASS, keyed by the request's engine).

func IsEngineName added in v0.2026265.2315

func IsEngineName(name string) bool

IsEngineName reports whether name is a member of the closed engine vocabulary. It does NOT resolve "auto" (that is a selector, not an engine word) — callers validating authored config reject "auto" here and resolve it separately.

func IsHostNetworked

func IsHostNetworked(engine, containerName string) bool

IsHostNetworked checks if a running container uses --network host.

func IsRunMode added in v0.2026265.2315

func IsRunMode(mode string) bool

IsRunMode reports whether mode is a member of the closed run-mode vocabulary.

func IsUnitRunMode added in v0.2026265.2315

func IsUnitRunMode(mode string) bool

IsUnitRunMode reports whether mode is supervised by a generated unit file (quadlet / systemd-unit) rather than an ephemeral argv launch (direct). The unit-capable subset is CUE-owned (#EngineUnitRunModes → spec.EngineUnitRunModeWords), so no caller hand-lists {"quadlet","systemd-unit"}.

func LooksLikeFullRef

func LooksLikeFullRef(ref string) bool

LooksLikeFullRef returns true if the image ref contains a registry segment (a "/" before any ":") — e.g. "ghcr.io/org/name:tag" — so it can be pulled without charly.yml resolution.

func ParsePublishedPort

func ParsePublishedPort(output string, port int) (string, error)

ParsePublishedPort extracts the host "ip:port" from `<engine> port` output for an in-container port, normalizing 0.0.0.0 / [::] to 127.0.0.1.

func ResolveBuiltImageRef added in v0.2026227.1655

func ResolveBuiltImageRef(engine, input string) (string, error)

ResolveBuiltImageRef resolves like ResolveLocalImageRef and then REFUSES the resolution when the elected image is not the newest local BUILD of that short name.

It is the resolver for every verb that pronounces a VERDICT on a built artifact — `charly check box`, `charly box feature run`, `charly box labels`. Those verbs answer "what is in the image I just built", and the election cannot answer that on its own: its PRIMARY key is the content-derived `ai.opencharly.version` label, so an image built from a differently-versioned source tree outranks a newer build regardless of when either was produced. A green verdict against the older artifact is the worst failure this system has — it looks exactly like a passing one — so the ambiguity is surfaced as an error and the operator names the artifact.

A full ref or an explicit `<name>:<tag>` states the intent and passes through untouched, which is why the R10 bed sequence (which builds and then checks `<image>:<run-tag>`) never reaches this guard.

func ResolveDeliverableRef added in v0.2026230.2320

func ResolveDeliverableRef(engine, input string) (string, error)

ResolveDeliverableRef resolves the image an image-DELIVERY verb should ship: an explicit ref that exists locally is used exactly as authored — naming a tag IS the choice — and anything else resolves through the STRICT ResolveBuiltImageRef, which refuses to elect an image older than the newest local build.

It exists because both delivery verbs need exactly this and had grown their own copy: `charly vm cp-box` (VM venue) and `charly box load` (container venue). The bodies were structurally identical and their justification comments near-verbatim rewords — in a cutover whose own thesis is that a second venue costs a constructor, not a copy (R3).

The strictness is the point, and it is easy to under-rate: delivering a stale artifact is the wrong-artifact class `charly check box` refuses to certify, and it is HARDER to notice at a delivery verb than at a build one, because the load succeeds and the venue simply runs the wrong image.

func ResolveLocalImageRef

func ResolveLocalImageRef(engine, input string) (string, error)

ResolveLocalImageRef resolves a user-supplied image reference against the engine's local storage — never reads charly.yml. Used by test-mode commands (charly check live, charly check box) so they stay within the test-mode input set.

For full refs (registry prefix present) it validates the image exists locally and passes through unchanged. For short names it resolves via CalVer: collect every candidate ref and pick the one with the highest `ai.opencharly.version` label (falling back to the highest tag CalVer). charly is CalVer-only — no `:latest` fallback. See `/charly-build:build` "CalVer-only" for the contract.

A candidate ref must satisfy BOTH halves: its image is identified as the short name (preferred: an `ai.opencharly.image=<short>` label; fallback: no such label) AND the ref ITSELF is named `<short>` in its trailing repo component. The second half is not cosmetic — a label-family image accumulates the deploy-name alias tags of every OTHER deployment built on the same base box, and they inherit its label, so without it an untagged resolve can return a sibling deployment's image (see the invariant comment in the label branch below).

Returns `spec.ErrImageNotLocal` when nothing matches. An ambiguous result across multiple repos with the same highest CalVer tag surfaces as an explicit error asking for a full ref.

This is the LENIENT form: it elects a ref and says nothing about what it passed over. A verb that pronounces a VERDICT on the artifact (`charly check box`, `charly box feature run`, `charly box labels`) must call ResolveBuiltImageRef instead, which refuses to elect an image older than the newest local build.

func ResolveNewestLocalCalVer

func ResolveNewestLocalCalVer(engine, short string) (string, error)

ResolveNewestLocalCalVer is the canonical "find the newest local image for this short name" helper. Thin wrapper around ResolveLocalImageRef — exposed so callers that start with an explicit short-name + empty-tag can resolve uniformly.

func ResolveShellImageRef

func ResolveShellImageRef(registry, name, tag string) string

ResolveShellImageRef builds the full image reference from registry, name, and tag, for a caller about to run an engine command (podman/docker) against it. When tag is empty, it resolves to the newest local CalVer for the given short name via ResolveNewestLocalCalVer — the CalVer-only contract (`/charly-build:build` "Cache Efficiency"). A caller that wants a specific tag passes it; a caller whose `--tag` flag is empty gets the newest CalVer with no extra work. When registry is set AND tag is empty, there's no way to guess a remote CalVer without a registry-list call, so the caller gets `<registry>/<name>` back with no tag suffix — the engine resolves it locally first (matching any single local tag) or errors. (P14: relocated from charly/shell.go's resolveShellImageRef, which now delegates here — R3, single source for every caller: candy/plugin-box's `merge` command plus charly core's own deploy_add/config_image/ensure_image/remote_image/pod_lifecycle_resolve/update_deploy_dispatch.)

func RunModes added in v0.2026265.2315

func RunModes() []string

RunModes returns the closed run-mode vocabulary (quadlet/systemd-unit/direct).

func StreamLoad added in v0.2026230.1417

func StreamLoad(save, load *exec.Cmd) error

StreamLoad pipes a `save` command's stdout straight into a `load` command's stdin — an image transfer with NO intermediate tarball on either side. It is the ONE streaming primitive behind every image-delivery path in the tree: TransferImage (engine→engine on one host), kit.TransferToRootful (rootless→rootful), the VM host→guest cp-box, and the container-venue host→nested-store `charly box load`. Each caller supplies its own pair of commands; the only thing they ever differed in was how the load side is reached, so that — and nothing else — stays at the call site.

A tarball is not merely wasteful here: a multi-GB image cannot land in a size-limited tmpfs /tmp, which is what the destination of several of these paths actually has.

Both commands' stderr is routed to the caller's stderr so a failure is visible rather than swallowed, and the load side's stdout goes there too — `podman load` writes its "Loaded image:" line to stdout, which is diagnostic output, not a value any caller parses.

NOTE for callers: `podman load` can exit 0 on a TRUNCATED stream, registering an image whose overlay layers are incomplete. A green return from StreamLoad is therefore NOT proof of an intact image; a caller that cares must probe the loaded image afterwards. deploykit's venue transfer does exactly that.

func TransferImage

func TransferImage(srcEngine, dstEngine, imageRef string) error

TransferImage pipes an image from one engine to another via save | load.

Types

type LocalImageInfo

type LocalImageInfo struct {
	ID     string            // image ID (sha256:...) — used by `charly clean` to skip in-use images
	Names  []string          // Full refs: ["ghcr.io/opencharly/jupyter:latest", ...]
	Labels map[string]string // OCI labels from the image config
	Size   int64             // reported storage size in bytes (podman's "Size" field; 0 if absent/unparsed)
	// Created is the image's creation time (podman/docker "Created", unix seconds; "CreatedAt" is
	// the RFC3339 rendering of the same instant). 0 when absent/unparsed.
	//
	// This is the ONLY build-recency key that is TOTAL over the tags charly mints. `charly box
	// build --tag <x>` REPLACES the CalVer tag rather than adding to it, so every bed build carries
	// exactly one tag of the form `check-<bed>-<YYYY.DDD.HHMM>` — which ExtractCalVerTag reports as
	// EMPTY, because it is not three decimal dot-parts. Ordering by the tag therefore ties every
	// bed-built candidate and falls through to a meaningless last resort. Creation time does not
	// tie, needs no tag convention, and costs nothing: it rides the same `images --format json`
	// rows Size already comes from.
	Created int64
}

LocalImageInfo describes an image present in the engine's local storage. Populated by ListLocalImages from `{podman,docker} images --format json`.

func ParseLocalImagesJSON

func ParseLocalImagesJSON(out []byte) ([]LocalImageInfo, error)

ParseLocalImagesJSON parses `{podman,docker} images --format json` output into ONE LocalImageInfo per distinct image ID, with that id's tag refs merged into Names.

This dedup is load-bearing: podman emits ONE ROW PER TAG, and each row's Names array already lists EVERY tag on that id. A naive row-by-row mapping therefore produces N near-identical entries for an id with N tags — which made `charly clean`'s keep-N retention over-count entries and, worse, remove an id's whole Names array per "extra" entry (deleting tags it meant to keep). Collapsing to one-id-with-a-tag-list matches the struct's shape and what every consumer (retention prune, short-name resolver) expects.

Empty-id rows (dangling/untagged) are kept separate via a per-row sentinel key so they never merge into one another.

The two engines emit DIFFERENT JSON SHAPES for the same command:

  • podman emits a single JSON ARRAY (`[{…},{…}]`);
  • docker emits JSON LINES — one object per line, NOT an array (`{…}\n{…}`).

Both are accepted, so `charly clean` (and the short-name resolver) work on either engine. A whole-buffer Unmarshal handles the array; a fallback per-line decode handles the docker NDJSON stream.

type LocalImageResolution added in v0.2026227.1655

type LocalImageResolution struct {
	// Ref is the elected reference — what ResolveLocalImageRef returns.
	Ref string
	// NewestBuildRef is the ref of the most recently CREATED candidate across BOTH families
	// (label-identified AND name-identified), so a build whose `ai.opencharly.box` label disagrees
	// with its own repo name is still counted. Empty only when the ordering could not be
	// established at all (see OrderKnown).
	NewestBuildRef string
	// SameArtifact reports that Ref and NewestBuildRef name the SAME image ID. Many tags on one id
	// are one artifact — a `--tag` build and a plain CalVer build of identical content share an id
	// — so there is no older/newer to choose between them and nothing to refuse.
	SameArtifact bool
	// OrderKnown reports that every candidate carried a creation time, i.e. that "newest" is a
	// fact rather than a guess. False means the engine did not report one for some candidate; the
	// guard then REFUSES rather than passing, because permissive-on-unknown is precisely the shape
	// that let a stale artifact through before.
	OrderKnown bool
	// Pinned reports that the input named a full ref or an explicit tag: the operator stated
	// which artifact they meant, so no election happened and nothing is ambiguous.
	Pinned bool
}

LocalImageResolution is the COMPLETE answer to "which local image does this reference name?": the ref the ordering elects (Ref) plus the newest local BUILD carrying the same short name (NewestBuildRef). The two diverge whenever the ordering's PRIMARY key — the content-derived `ai.opencharly.version` label — elects an image that is not the most recently built one, which is exactly the shape that makes a build-scope verdict certify the wrong artifact. The lenient ResolveLocalImageRef keeps only Ref; a verb that PRONOUNCES on an artifact resolves through ResolveBuiltImageRef instead, which refuses that divergence.

func ResolveLocalImage added in v0.2026227.1655

func ResolveLocalImage(engine, input string) (LocalImageResolution, error)

ResolveLocalImage is ResolveLocalImageRef's full-answer form — same election, same errors, plus the newest-build ref the election may have passed over. Every caller that only needs the elected ref goes through ResolveLocalImageRef; the build-scope verdict verbs go through ResolveBuiltImageRef. Both are thin wrappers over this one body (R3 — one resolution, one candidate gather, one ordering).

func (LocalImageResolution) RefuseIfStale added in v0.2026227.1655

func (r LocalImageResolution) RefuseIfStale(input string) error

RefuseIfStale returns a `spec.ErrStaleLocalImage`-wrapped error when the elected ref is older (by build tag) than the newest local build of the same short name. Pinned resolutions never refuse: the operator already said which artifact they meant.

Jump to

Keyboard shortcuts

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