Documentation
¶
Overview ¶
Package publish implements `aileron skill publish`: pushing a frozen Flight Plan's composed (or base) image and its signed artifact to an OCI registry so a second operator on another machine can install and launch it without re-freezing (umbrella #1898, the write half of cross-machine sharing).
Freeze stays local and offline (ADR-0027); publish is the explicit, network-touching act. The signed ed25519 artifact remains the root of trust: publish is a pass-through push of the already-signed bytes, and the consumer verifies signature + content hash + publisher trust at install/launch.
The digest binding that lets launch (#1903) verify the pulled image against the signed lock branches by pin type (see freeze.BindingKind):
- Composed-tools pins (LocalTag set): the image is built locally at freeze for every supported architecture into an OCI image-layout directory and pinned by a per-arch set of serialization-agnostic config CONTENT digests (see internal/flightplan/imgconfig, ADR-0027). Publish opens that layout (never the docker daemon, which cannot hold a manifest list), verifies EVERY arch's config content digest against the signed lock and HARD-ERRORS on any mismatch or missing arch BEFORE pushing, then copies the whole manifest-list graph into the destination registry with oras and re-verifies every pushed arch's config content digest. The content digest survives the benign config-blob re-serialization a registry may perform (issue #2014), unlike the raw config-blob sha256, so a genuine per-arch field substitution is caught on both sides while a re-encode passes. Binding: config-content-digest.
- Image-only / custom-base pins (no LocalTag): the signed-lock Digest is the base image's registry manifest digest. Publish copies the exact bytes from the SOURCE registry with oras.Copy (which preserves the manifest digest) into the target registry. Copying a docker-re-encoded export would change the manifest digest and defeat the binding, so the foreign-base path copies registry bytes directly and never round-trips through the docker daemon. Binding: manifest-digest.
All oras/registry construction lives here so cmd/aileron stays oras-free.
Index ¶
Constants ¶
This section is empty.
Variables ¶
var ( // ErrNoImage is returned when the frozen version has no resolved image pin // to publish (an instruction-only plan). ErrNoImage = errors.New("publish: frozen version has no resolved image to publish") // ErrConfigContentDigestMismatch is returned when a composed pin's per-arch // config content digest set does not match the signed lock, on either the // local layout (pre-push) or the pushed manifest list (post-push): a // mismatched arch, an arch the lock does not attest, or a lock-pinned arch the // artifact is missing. It fails closed rather than shipping a binding #1903 // would reject. Because the check is content-based, the benign config-blob // re-serialization a registry may perform does not trigger it; only a genuine // execution-relevant field change does. ErrConfigContentDigestMismatch = errors.New("publish: composed image config content digest does not match the signed lock") )
Errors returned for the documented failure modes.
Functions ¶
This section is empty.
Types ¶
type Options ¶
type Options struct {
// Name and VersionID identify the frozen version being published; VersionID
// is also the artifact's OCI tag under Registry.
Name string
VersionID string
// Registry is the destination OCI repository (e.g. "ghcr.io/acme/plan").
// The composed/base image and the signed-artifact referrer both land here.
Registry string
// Frozen is the frozen version's on-disk artifact bytes (SKILL.md, lock,
// signature, public key). Lock is the parsed lockfile (freeze.ParseLockfile
// over Frozen.Lockfile), carrying the image pin to publish.
Frozen store.FrozenVersion
Lock freeze.Lockfile
// ComposedLayout opens the freeze-produced multi-arch OCI image layout for a
// composed pin, returning a read-only store to copy/verify from and its root
// manifest-list descriptor. Nil selects the production opener, which derives
// the layout dir from the pin's LocalTag (composition.OCILayoutDir) and opens
// it (ociremote.OpenOCILayout). Tests inject a synthetic multi-arch index in an
// in-memory store, so the push + per-arch verify contract is exercised with no
// docker daemon and no cross-arch emulation. Mirrors the SourceRepo seam.
ComposedLayout func(ctx context.Context, pin freeze.ImagePin) (oras.ReadOnlyTarget, ocispec.Descriptor, error)
// Target is the destination content store. Nil builds a remote.Repository
// from Registry over the operator's docker credentials. Tests inject an
// in-memory store.
Target oras.Target
// SourceRepo resolves a foreign-base pin ref to the source content store to
// copy from. Nil builds a remote.Repository from the pin ref's registry
// over the operator's docker credentials. Tests inject an in-memory store.
SourceRepo func(ctx context.Context, ref string) (oras.ReadOnlyTarget, error)
Stdout io.Writer
Stderr io.Writer
// Quiet suppresses the live push-progress feedback (the liveness spinner) on
// both the TTY and non-TTY paths. The `published ...` summary and its
// install hint still print: they are the result, not progress output.
Quiet bool
}
Options configures a publish run.
type Result ¶
type Result struct {
// ImageRef is the pushed image coordinate ("<registry>@<digest>").
ImageRef string
// ImageDigest is the pushed image's descriptor digest.
ImageDigest string
// ArtifactRef is the signed-artifact reference the consumer installs
// ("<registry>:<version>").
ArtifactRef string
// ArtifactDigest is the artifact (referrers) manifest digest; stable across
// idempotent re-publishes of the same frozen version.
ArtifactDigest string
// BindingKind is "config-content-digest" or "manifest-digest"
// (freeze.BindingKind).
BindingKind string
}
Result reports what a publish run pushed.
func Run ¶
Run publishes opts.Frozen's image and signed artifact to opts.Registry. It is a thin wrapper over run that pipes any returned error through annotateRegistryAuthError, so a registry auth/scope rejection (e.g. ghcr.io's raw "permission_denied ... expected scopes" from docker push) reaches the operator with an actionable hint instead of the opaque registry string. Applying it at this boundary covers auth failures from every push path underneath: the composed-image push, the foreign-base copy, and the artifact-blob pushes.