Documentation
¶
Overview ¶
Package container is the spec fabric slice for container-engine host helpers — resolving which engine binary to invoke, its GPU run-args, and auto-detecting the installed engine. 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.
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 ¶
- Variables
- func ContainerImage(engine, containerName string) string
- func ContainerImageRef(engine, containerName string) (string, error)
- func DetectEngine() (string, error)
- func EngineBinary(engine string) string
- func ExtractCalVerTag(ref string) string
- func ExtractMetadata(engine, imageRef string) (*spec.BoxMetadata, error)
- func GPURunArgs(engine string) []string
- func InspectImageLabels(engine, imageRef string) (map[string]string, error)
- func IsHostNetworked(engine, containerName string) bool
- func LooksLikeFullRef(ref string) bool
- func ParsePublishedPort(output string, port int) (string, error)
- func ResolveBuiltImageRef(engine, input string) (string, error)
- func ResolveLocalImageRef(engine, input string) (string, error)
- func ResolveNewestLocalCalVer(engine, short string) (string, error)
- func ResolveShellImageRef(registry, name, tag string) string
- func StreamLoad(save, load *exec.Cmd) error
- func TransferImage(srcEngine, dstEngine, imageRef string) error
- type LocalImageInfo
- type LocalImageResolution
Constants ¶
This section is empty.
Variables ¶
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.
var ListLocalImages = defaultListLocalImages
ListLocalImages returns all images in the engine's local storage. Package-level var for testability (same pattern as LocalImageExists, DetectEngine).
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 ¶
ContainerImage returns the image ref for a running container, best-effort ("" on error). Thin wrapper over ContainerImageRef.
func ContainerImageRef ¶
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 ¶
DetectEngine auto-detects the container engine: prefers podman, falls back to docker.
func EngineBinary ¶
EngineBinary returns the CLI binary name for a container engine. "auto" resolves via DetectEngine (podman preferred), falling back to "docker".
func ExtractCalVerTag ¶
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 ¶
GPURunArgs returns the engine-specific run flags that expose all host GPUs to a container.
func InspectImageLabels ¶
InspectImageLabels reads a local image's OCI labels via engine inspect. Pure container-storage probe: no charly-core coupling.
func IsHostNetworked ¶
IsHostNetworked checks if a running container uses --network host.
func LooksLikeFullRef ¶
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 ¶
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
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 ResolveLocalImageRef ¶
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 ¶
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 ¶
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 fleet_add/config_image/ensure_image/remote_image/pod_lifecycle_resolve/update_deploy_dispatch.)
func StreamLoad ¶ added in v0.2026230.1417
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 ¶
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.
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.