Documentation
¶
Overview ¶
Package bundle produces and consumes the artifact that answers "exactly what bytes, from exactly which source" for one environment.
A bundle is one env's render, from one checkout, pinned to one release, packaged as an immutable OCI artifact (doc §4). It is what "what was shipped" means: `forge env build` writes one, the deploy path applies FROM it rather than from a second render, and a ledger records its digest. That is the change that ends the class of bugs where `env render` and `env deploy` rendered subtly differently — "what was applied" and "what was recorded" become the same bytes by construction.
The two halves ¶
PRODUCTION. Build takes one env's render and returns the artifact: a deterministic manifest layer, the release.BundleDoc config blob, the config digest, and the OCI manifest binding them. Push writes it to a registry; LocalLayout writes it to an OCI image layout on disk, which is where a local env's bundle goes because it is not shipped.
CONSUMPTION. Fetch pulls one back, verifying every byte against the digest that named it. Unpack extracts a layer under the hostile-input discipline in unpack.go. Cache keys unpacked bundles by digest, which is what makes re-applying an unchanged bundle cost one HTTP HEAD.
ProjectShape is the projection both halves share: one env's render becomes the release.Shape a ledger indexes — what runs, where, which secrets it needs, which domains it binds, and one hash per object.
Two invariants, both structural ¶
THE SHAPE AND THE MANIFESTS CANNOT DISAGREE. Both come from one parse of one stream (parse.go). A bundle whose shape described objects its layer did not carry would be a record of a deploy that never happened, and nothing could detect it — both halves are sealed under the same digest, so they would be consistently wrong.
NO SECRET VALUE REACHES A BUNDLE (F-13). parseStream redacts on the way through, so no unredacted document is available to be packaged at all, and Build re-checks the final bytes before returning. Bundles are pushed, cached on every machine that applies them, and kept forever; a value in one is a permanent leak.
The dependency direction ¶
ONE WAY: internal/cli → internal/bundle, never back. This package knows nothing about KCL, cobra, a project on disk, or a control plane. It takes a rendered manifest stream plus the few facts the stream does not carry, and returns a value. That is what makes it testable from a literal fixture, and it is what stops "which cluster does this document land on" from being answered twice: the routing stays in internal/cluster, and this package reads the answer off the stream.
It also does not import the machine ledger. NewLocalLayout takes its root as an argument, so where bundles live on disk stays one decision made in one place — in internal/ledgerfile, which depends on this package rather than the other way round.
Index ¶
- Constants
- Variables
- func ChangedFields(live, candidate any) []string
- func NewRepository(ref string) (*remote.Repository, error)
- func NewRepositoryWithCredential(ref string, credential auth.CredentialFunc) (*remote.Repository, error)
- func Objects(f Fetched) (map[release.ObjectKey]any, error)
- func ProjectShape(in ShapeInput) (release.Shape, error)
- func Push(ctx context.Context, target Pusher, b Bundle) (string, error)
- func Repository(imagePushBase, env string) string
- func Republish(ctx context.Context, src Resolver, dst Pusher, digest string) (string, error)
- func SnapshotInventory(ctx context.Context, in BuildInput, declaration release.Snapshot) (release.Snapshot, error)
- func SnapshotStream(s release.Snapshot) string
- func Unpack(r io.Reader, destDir string) ([]string, error)
- type BuildInput
- type Bundle
- type Cache
- type Cached
- type Fetched
- type LocalLayout
- func (l *LocalLayout) Fetch(ctx context.Context, target ocispec.Descriptor) (io.ReadCloser, error)
- func (l *LocalLayout) Reference(digest string) string
- func (l *LocalLayout) Resolve(ctx context.Context, reference string) (ocispec.Descriptor, error)
- func (l *LocalLayout) Root() string
- func (l *LocalLayout) Write(ctx context.Context, b Bundle) (string, error)
- type ObservabilityFile
- type PlacementDeclaration
- type Pusher
- type RenderedObject
- type Resolver
- type SecretDocument
- type ShapeInput
- type Stats
Constants ¶
const ( AnnotationEnv = "dev.forge.env" AnnotationProject = "dev.forge.project" AnnotationRelease = "dev.forge.release" AnnotationConfigDigest = "dev.forge.config-digest" AnnotationDirty = "dev.forge.dirty" AnnotationWorktreeKey = "dev.forge.worktree-key" )
Annotation keys on a bundle's OCI manifest (doc §4.1). The `org.*` ones are OCI standard, so a registry UI shows them; the `dev.forge.*` ones are what the retention policy and a human browsing a repository read.
const ( // MaxLayerBytes caps total decompressed output per layer. MaxLayerBytes int64 = 64 << 20 // MaxEntries caps the entry count, which the byte ceiling does not // bound (a million zero-length files cost no bytes). MaxEntries = 4096 // MaxFileBytes caps any SINGLE file. Redundant against MaxLayerBytes // for a one-file bomb and not redundant for the shape that matters: it // localises the error to the offending entry instead of reporting "the // archive is too big" after a well-formed archive was half written. It is // the shared layer contract, [release.ManifestsLayerMaxBytes], because an // artifact (a service catalog) may be that large and still validate: a // bundle forge builds and the control plane admits must also unpack. MaxFileBytes int64 = release.ManifestsLayerMaxBytes // MaxManifestBytes caps an OCI manifest and a config blob. The server // bounds both at 1 MiB (doc §6.3's RecordBundle), and forge refuses // the same size rather than a larger one: a bundle forge would push // and the server would refuse is a build that fails at the last step. MaxManifestBytes int64 = 1 << 20 )
const CacheDirRel = statefile.DirRel + "/bundles"
CacheDirRel is where unpacked bundles live, under the same .forge/ tree as every other piece of forge runtime state (so the single existing .forge/ gitignore rule already covers it).
const RepositorySegment = "bundle.v1"
RepositorySegment is the path segment bundles live under, appended to an env's image push base: `<image_push_base>/bundle.v1/<env>`.
THE DOT IS LOAD-BEARING, exactly as it is for static sites: an RFC-1123 build name cannot contain one, so no backend image an org builds can collide with a bundle repository.
Variables ¶
var ErrNotBundle = errors.New("forge: artifact is not a forge bundle")
ErrNotBundle is the refusal for an artifact that is not a bundle: a reference that resolves to something else is a configuration mistake, not a corrupt bundle, and a caller should be able to tell those apart.
var ErrSecretValue = errors.New("forge: refusing to write a bundle carrying a Secret value")
ErrSecretValue is the refusal F-13 names. Separate from release.ErrInvalid so a caller can tell "this render is malformed" from "this build would have leaked a secret", which are a bug report and an incident respectively.
var ErrUnsafeArchive = errors.New("forge: refusing to unpack bundle")
ErrUnsafeArchive is the sentinel every defensive refusal wraps, so a caller can distinguish "this bundle is hostile or corrupt" from an ordinary IO failure.
Functions ¶
func ChangedFields ¶
ChangedFields names every field two documents differ in, as dotted paths, sorted. A subtree present on one side only is named once, at its root.
A list whose items are all objects carrying a string `name` — containers, env vars, ports, volumes — is compared BY NAME and its items named by it (`spec.template.spec.containers[api].image`): an index would shift when an item is inserted, and every later item would read as changed.
func NewRepository ¶
func NewRepository(ref string) (*remote.Repository, error)
NewRepository builds a registry client for `registry/repo` (no tag — the tag or digest is passed to Resolve), authenticated from the ambient docker credential store — what a `docker push` to the same host would present.
The transport retries on the registry's own throttling responses. That is a default rather than forge policy: a loop that polls a registry WILL eventually be rate-limited, and a client that treated a 429 as a hard failure would report an env as unobservable for the duration.
A hosted env's bundle does NOT come through here: its credential is the control-plane identity, not whatever a store happens to hold for the host (see NewRepositoryWithCredential).
func NewRepositoryWithCredential ¶
func NewRepositoryWithCredential(ref string, credential auth.CredentialFunc) (*remote.Repository, error)
NewRepositoryWithCredential is NewRepository with the credential supplied by the caller instead of read from the docker credential store.
THE STORE IS NEVER CONSULTED. That is the point: a store entry is a long-lived value some earlier login left behind, and when it is stale it shadows everything fresher — the registry's realm rejects it and the push fails with a 401 no amount of correct credentials elsewhere can fix. A caller that holds the right identity in memory passes it here and the store cannot interfere, in either direction: nothing is read from it and nothing is written to it.
The credential is exchanged at the registry's token realm for a short-lived access token on the first challenge; that token is what travels with the blob and manifest requests.
func Objects ¶
Objects reads a fetched bundle's manifest layer into its documents, keyed exactly as the bundle's shape keys its objects (cluster, kind, namespace, name), so a plan finding's subject finds its document.
The cluster comes from the tree the document sits in, as the bundle's own ClusterPaths names it; a file outside every named tree is skipped rather than attributed to a guessed cluster. It applies Unpack's bounds — the layer is as hostile here as anywhere else.
func ProjectShape ¶
func ProjectShape(in ShapeInput) (release.Shape, error)
ProjectShape projects one env's render into the shape a ledger indexes.
It is the ONE projection: `forge env shape`, the declaration `forge env build` records, and the bundle F2 writes all call this, so a shape shown to a user, a shape stored on an env and a shape sealed inside a bundle cannot describe the render differently.
NO SECRET VALUE SURVIVES IT. Every `kind: Secret` document has its `data` / `stringData` values replaced by the hash of the value BEFORE the document is hashed, so neither the shape nor any hash input carries one (F-13). The hash still moves when a value moves, which is what keeps "a secret changed" visible without the secret.
func Push ¶
Push writes a built bundle's blobs and manifest to a target, and returns the manifest digest.
`already exists` is SUCCESS, not an error. A bundle's digest is a pure function of its content, so a re-push of an unchanged render is the same artifact: treating the collision as a failure would make an idempotent operation look broken and would push callers toward "push once and hope" rather than "push whenever unsure".
Blobs go before the manifest, because a manifest that referenced a blob the registry did not yet hold would be a dangling artifact — resolvable, recordable, and unfetchable. That is doc §13's F-4 shape, created by the pusher rather than by a GC bug.
func Repository ¶
Repository is where an env's bundles are pushed.
The segment is APPENDED rather than asked for, so it never appears in anyone's KCL: which repository a bundle lands in is forge's layout decision, not something an author should have to encode.
func Republish ¶
Republish copies the bundle at `digest` from `src` to `dst` and returns the digest it wrote.
EVERY BYTE IS VERIFIED ON THE WAY THROUGH, by the same [readBlob] the registry fetch path uses: each blob is hashed as it is read and the read fails if the hash diverges. That is not belt-and-braces over a local directory — it is what makes the claim "Flux applies the bytes the ledger recorded" true rather than assumed, and the local layout is a directory a human can edit.
The returned digest is the one actually written, and a caller should pin its pointer to THAT rather than to the digest it asked for. They are equal — the manifest is read by digest and pushed unmodified — and returning it keeps the caller from having to take that on trust.
func SnapshotInventory ¶ added in v0.1.46
func SnapshotInventory(ctx context.Context, in BuildInput, declaration release.Snapshot) (release.Snapshot, error)
SnapshotInventory captures the final candidate resource bytes, after the normal safety transforms. Explicit owner annotations win; unannotated objects are conservatively owned by every consumer on the destination, never guessed independent from a coincidental object name.
func SnapshotStream ¶ added in v0.1.46
SnapshotStream reconstructs an annotated apply stream without changing a single object's serialized bytes. It is not a fresh render from source.
func Unpack ¶
Unpack extracts a gzipped tar layer into destDir, enforcing every measure documented above. It creates destDir if absent.
Returns the extracted paths, relative to destDir and slash-separated, so a caller can see what a bundle actually contained without walking the tree again — and, for the manifest layer, apply them in that order.
Types ¶
type BuildInput ¶
type BuildInput struct {
// Snapshot is a complete composition. Its recorded object bytes, never a
// new checkout render, are packaged verbatim and checked against the layer.
Snapshot *release.Snapshot
// Project and Env name the bundle. Both required: a bundle is one
// env's render, and a reader that could not tell which env it
// described would have an artifact it could not place.
Project string
Env string
// Release is the version pinned, or "" for an unreleased bundle (dev,
// or a Preview deploy) pinned to the build's own digests.
Release string
// Pins is the pin set the render resolved: images by artifact key,
// and source pins for source-built components.
Pins release.BundlePins
// Provenance is where the render came from. Captured by the caller
// (release.CaptureProvenance), never by this package: provenance is a
// property of a checkout and this package never sees one.
Provenance release.Provenance
// Reader is what the backend this bundle is recorded in decodes. The
// document leaves out every optional part it does not name
// (release.BundleReader.Limit): a control plane strict-decodes the
// document, so a field it predates fails the whole record. Nil writes
// every part — the machine ledger is read by this same forge.
Reader *release.BundleReader
// Shape is the declaration half of the projection, exactly as
// [ProjectShape] takes it. Build runs the projection ITSELF, from the
// same parse it packages, rather than accepting a finished shape —
// which is what makes "the shape describes these manifests" true by
// construction instead of by a caller's discipline.
Shape ShapeInput
// CreatedAt is the bundle's creation time. Required, and the caller's:
// see the determinism note at the top of this file.
CreatedAt time.Time
// Reconciled says a reconciler (Flux) may apply this bundle, so it must be
// safe to apply repeatedly: a Job with a TTL or without a spec hash is then
// REFUSED rather than built. False for an env forge applies itself (a
// local or ephemeral lifecycle, or a declared applier of forge): a
// one-shot apply neither re-runs a TTL'd Job on a timer nor re-reconciles
// an unhashed one, and refusing there would break builds that work today.
// The fixes that change nothing at runtime are made either way.
Reconciled bool
// ReconciledUnits narrows the refusal to the delivery units a reconciler
// applies, for an env that moves onto Flux one unit at a time: an object in
// a unit forge still applies directly is not refused, so the first unit to
// move is not held up by a Job in one that has not. Consulted only when
// Reconciled is false; Reconciled true is every unit. The fixes that change
// nothing at runtime are made for every unit regardless.
ReconciledUnits []string
// Units declares the delivery units' order (see release.DeliveryUnit).
// An object's unit is its forge.dev/delivery-unit label; a unit nobody
// declares waits for the default unit. Every real cluster is split into
// trees by it; see release.PlanTrees.
Units []release.DeliveryUnit
// Placements is the complete logical dependency declaration for mixed bundles.
// Nil is unknown, not an assertion of independence.
Placements []PlacementDeclaration
// Observability is the dashboards and the metrics schema the bundle
// carries beside its manifests (see [release.BundleObservabilityPrefix]).
// Nothing applies them, so they neither enter the shape nor the config
// digest; they do enter the layer, and so the bundle's digest.
Observability []ObservabilityFile
}
BuildInput is one env's render plus the facts that identify it.
type Bundle ¶
type Bundle struct {
// Digest is the OCI manifest digest: the bundle's identity.
Digest string
// Doc is the config blob, decoded. [Bundle.ConfigBlob] is the bytes.
Doc release.BundleDoc
// Manifest is the OCI image manifest, as raw JSON — the bytes Digest
// is over, so a consumer verifies against exactly what was hashed
// rather than against a re-marshalling of the struct.
Manifest []byte
// GuardedObjects is how many of the layer's objects were stamped
// prune-exempt (prune_guard.go): the ones a reconciler must never
// delete as a side effect of a render that came out short.
//
// Carried so the build can SAY so. A guard nobody sees is one nobody
// notices has stopped working, and the day it matters is the day a
// PersistentVolumeClaim is already gone.
GuardedObjects int
// contains filtered or unexported fields
}
Bundle is a built bundle, in memory: its identity, its document, and the blobs that constitute it.
Held in memory rather than written to a path because the two things a caller does with it — push it to a registry, write it into a local OCI layout — both want the blobs, and a bundle is small by design (doc §4.1 measures control-plane prod at 246 KB).
func Build ¶
func Build(ctx context.Context, in BuildInput) (Bundle, error)
Build renders one env's bundle: the manifest layer, the shape, the config digest, the BundleDoc, and the OCI manifest that binds them.
It takes a context for symmetry with Push and because a large render is cancellable work; it performs no IO.
func (Bundle) ConfigBlob ¶
ConfigBlob is the BundleDoc's canonical bytes — what release.DecodeBundleDoc reads and what the config descriptor's digest is over.
type Cache ¶
type Cache struct {
// contains filtered or unexported fields
}
Cache is a digest-keyed store of unpacked bundles on local disk.
The zero value is not usable; construct with NewCache.
type Cached ¶
type Cached struct {
// Digest is the bundle's identity.
Digest string
// Doc is the bundle document.
Doc release.BundleDoc
// Dir is the directory the manifest layer was unpacked into:
// `manifests/<cluster>/…`.
Dir string
// ManifestFiles are the manifest-layer paths relative to Dir, in APPLY
// ORDER (the render order the filenames encode).
ManifestFiles []string
}
Cached is one unpacked bundle on disk.
type Fetched ¶
type Fetched struct {
// Digest is the OCI manifest digest the reference resolved to, and the
// digest the received bytes were verified against.
Digest string
// Doc is the config blob, strictly decoded.
Doc release.BundleDoc
// Manifests is the manifest layer, still packed. Unpack it with
// [Unpack] — which is where the hostile-input discipline lives, and
// the reason this is not handed back as a directory.
Manifests []byte
}
Fetched is a bundle pulled back from a registry: the verified document plus the layer bytes, with the digest it was fetched by.
func Fetch ¶
Fetch resolves a reference and pulls the bundle it names, verifying every byte against the digest that named it.
Four refusals, and each one closes a way the record could lie:
- a reference resolving to something whose artifactType is not a bundle's. A tag can be moved; the repository layout is a convention, not a guarantee, so the ARTIFACT has to say what it is.
- a digest that is not canonical `sha256:<64 hex>`. The platform's ledger enforces that form in SQL, so a looser rule here would let forge pull something the server could never have recorded.
- content that does not hash to its descriptor's digest. This is the one that matters: it is what makes "the recorded bytes" and "the applied bytes" the same claim.
- a manifest, config or layer over its bound. The server bounds the manifest and config at 1 MiB; forge refuses the same sizes.
type LocalLayout ¶
type LocalLayout struct {
// contains filtered or unexported fields
}
LocalLayout is an OCI image layout directory (`oci-layout` + `blobs/sha256/…` + `index.json`): where a bundle goes when it is not pushed.
A `local` env's bundle is written here even when its control plane is reachable, because it is not SHIPPED — doc §4.3. The machine ledger (F3) owns the directory and passes its path in; this type does not know where the ledger lives, and deliberately does not import it. The dependency runs ledger → bundle, so a bundle can be written to any layout a caller names and the layout root stays one decision made in one place.
func NewLocalLayout ¶
func NewLocalLayout(dir string) (*LocalLayout, error)
NewLocalLayout opens (or creates) an OCI image layout rooted at dir.
func (*LocalLayout) Fetch ¶
func (l *LocalLayout) Fetch(ctx context.Context, target ocispec.Descriptor) (io.ReadCloser, error)
Fetch reads one blob out of the local layout. See LocalLayout.Resolve for why both exist.
func (*LocalLayout) Reference ¶
func (l *LocalLayout) Reference(digest string) string
Reference is how a ledger records a bundle held in a local layout: `oci-layout:<dir>@<digest>`. Both halves are needed — the digest alone does not say which machine's layout holds the blobs, and a file ledger is machine-scoped.
func (*LocalLayout) Resolve ¶
func (l *LocalLayout) Resolve(ctx context.Context, reference string) (ocispec.Descriptor, error)
Resolve and Fetch make a LocalLayout a Resolver, so Fetch reads a local bundle through exactly the path a registry bundle goes through — same verification, same bounds, same refusals. A second read path for local bundles is how "it works from the registry but not from the layout" starts.
type ObservabilityFile ¶
type ObservabilityFile struct {
// Path is slash-separated and under [release.BundleObservabilityPrefix].
Path string
Data []byte
}
ObservabilityFile is one file under the bundle's observability/ directory.
type PlacementDeclaration ¶ added in v1.0.6
type PlacementDeclaration struct {
Workload string
Runtime string
Actuator string
Dependencies []string
Unit string
}
PlacementDeclaration is logical release dependency input, never a readiness claim. The actual keys and binding hashes are projected from the sealed parse by Build.
type Pusher ¶
type Pusher interface {
Push(ctx context.Context, expected ocispec.Descriptor, content io.Reader) error
}
Pusher is the slice of a registry Push needs: one method.
type RenderedObject ¶ added in v0.1.46
type RenderedObject struct {
// Clusters are the clusters the deploy layer routes the document to, from
// the stream's `# cluster:` header. Empty: attributed to none, so nothing
// applies it.
Clusters []string
APIVersion string
Kind string
Namespace string
Name string
// Body is the decoded document. It is nil for a Secret: a Secret never
// rides a bundle and its values are nobody's business here, so the body is
// not retained.
Body map[string]any
}
RenderedObject is one document of a render stream.
func ParseRenderedStream ¶ added in v0.1.46
func ParseRenderedStream(stream string) ([]RenderedObject, error)
ParseRenderedStream decodes an annotated render stream into its objects, in stream order.
type Resolver ¶
type Resolver interface {
// Resolve maps a reference (tag or digest) to its descriptor. This is
// the HEAD — it must not transfer the content.
Resolve(ctx context.Context, reference string) (ocispec.Descriptor, error)
// Fetch reads the content a descriptor names.
Fetch(ctx context.Context, target ocispec.Descriptor) (io.ReadCloser, error)
}
Resolver is the slice Fetch needs — one method for the HEAD and one for the pull. Anything that satisfies it works: oras's remote.Repository does, and so does the in-memory store the tests use.
type SecretDocument ¶
SecretDocument is one rendered `kind: Secret`, UNREDACTED, with the clusters the deploy layer routes it to. It exists for the deploy-time Secret sync and must never be written to a bundle.
func SecretDocuments ¶
func SecretDocuments(stream string) ([]SecretDocument, error)
SecretDocuments extracts the Secret documents of a rendered stream with their values intact, in stream order.
type ShapeInput ¶
type ShapeInput struct {
// Kind is the env kind the render implies (forge's hostedEnvKindOf
// predicate, in pkg/release vocabulary). Required.
Kind release.EnvKind
// Workloads, Secrets, Domains and Clusters are the declaration's own
// projection, already in shape vocabulary. Secrets carry NAMES.
Workloads []release.ShapeWorkload
Secrets []release.ShapeSecret
Domains []string
Clusters []string
// Manifests is the env's rendered manifest stream, exactly as
// `forge env render <env>` prints it: `---`-separated documents, each
// optionally preceded by the `# cluster: <a>, <b>` comment naming the
// cluster(s) a deploy routes it to. A document with no such comment,
// or one naming none, is attributed to no cluster rather than to a
// guessed one.
Manifests string
// SyncedSecrets names Secrets the env needs that forge syncs from its
// secret store and that therefore never appear in Manifests (a
// secret_ref projection, a declared rendered Secret). Names only. They
// are recorded in the bundle beside the render's own Secret documents.
SyncedSecrets []release.BundleSecretRef
// Images maps each release artifact key to the digest this render
// pinned it to. It is what makes [release.ShapeObject.Images] and
// ConfigHash possible: an object's images are the pins that appear in
// it, and its ConfigHash is its hash with those pins normalized away.
Images map[string]string
// StatefulWorkloads names the workloads whose objects hold data beyond
// their own spec — in practice the env's declared ManagedDatabases,
// whose records expand into a Postgres cluster, its Services and its
// volumes. The well-known stateful kinds (PVC, StatefulSet, Secret, …)
// are recognised by pkg/release without being listed here; this is for
// the objects whose kind alone does not say so.
StatefulWorkloads []string
// ControlPlaneSecrets are the Secrets the env's control plane supplies and
// renews (forge.ControlPlaneSecret), already in shape vocabulary. Names and
// destinations; a value never reaches forge.
ControlPlaneSecrets []release.ShapeControlPlaneSecret
// Store is the env's store-backed secrets and the components that serve
// them (release.ProjectStoreSecrets over the render), already in shape
// vocabulary. The zero value records nothing.
Store release.StoreProjection
// Alerts and AlertRoutes are the env's resolved alert rules and routes,
// already in shape vocabulary (see release.ShapeAlert).
Alerts []release.ShapeAlert
AlertRoutes []release.ShapeAlertRoute
// ObservabilityPolicy is the env's observability opt-out, or nil when it
// opts out of nothing.
ObservabilityPolicy *release.ShapeObservabilityPolicy
// FrontendTelemetry is what each frontend declares about its browser
// telemetry, or nil.
FrontendTelemetry []release.ShapeFrontendTelemetry
// DeliveryPolicy is the env's delivery policy, or nil for the defaults.
DeliveryPolicy *release.ShapeDeliveryPolicy
// DataAccess is the env's declared data access, or nil for the default.
DataAccess *release.ShapeDataAccess
// ReleaseHealth is the env's release-health policy, or nil for the
// defaults.
ReleaseHealth *release.ShapeReleaseHealth
// UptimeChecks are the env's uptime checks, or nil when it has none.
UptimeChecks []release.ShapeUptimeCheck
// Identity is the env's managed identity declaration, or nil when it
// declares none.
Identity *release.ShapeIdentity
// Databases is what the env declares about its managed databases beyond
// their workload entries (live dashboards), or nil when it declares none.
Databases []release.ShapeDatabase
// Applier is the env's declared applier (release.ApplierForge or
// release.ApplierFlux), or "".
Applier string
// Units is who applies each delivery unit, when the env declares a split.
Units []release.ShapeUnit
}
ShapeInput is one env's render, as the projection needs it.
The caller supplies two kinds of thing, and the split is deliberate. The DECLARATION half (kind, workloads, secrets, domains, clusters) is read off the rendered entity contract, which only internal/cli can decode. The MANIFEST half is the rendered stream itself, which this package parses, so the bytes that get hashed are the bytes a deploy applies — not a second model of them.
type Stats ¶
type Stats struct {
// Resolved is true when the reference was resolved (the HEAD). Always
// true on success.
Resolved bool
// Pulled is true when content was transferred. FALSE ON A CACHE HIT —
// that is the property worth asserting.
Pulled bool
// Digest is the resolved manifest digest.
Digest string
}
Stats records what one Cache.Get actually did, so a caller (and a test) can tell a cache hit from a pull WITHOUT inferring it from timing.
This exists because "an unchanged digest must cost one HEAD, not a full pull" is invisible from the outside: both paths return the same bundle. A test asserting the hit path by measuring duration would pass against an implementation that pulled every time, on a fast enough machine. Recording the decision makes the property checkable rather than believed.