Documentation
¶
Overview ¶
Package release defines the wire contract between the shunt CLI and the shunt-helper binary that runs on the host. The CLI resolves a manifest plus secrets into a Spec, streams it over stdin (never argv, never a temp file), and the helper applies it and emits Events on stdout as NDJSON.
Index ¶
- Constants
- func HashSecret(salt, v string) string
- func HashService(svc Service) string
- func HealthProbePath(svc Service) string
- func NewSalt() (string, error)
- func ProxyGatesReadiness(svc Service) bool
- func ScopeDigest(scope []string) string
- func TreeSummary(root string) (bytes int64, files int, newest int64)
- type Artifact
- type Entry
- type Event
- type Health
- type ImageRef
- type Ledger
- type Provenance
- type Proxy
- type Service
- type Spec
- type Stage
Constants ¶
const ( StatusActive = "active" // currently serving StatusSuperseded = "superseded" // replaced by a later release StatusFailed = "failed" // failed before any running container was replaced StatusDegraded = "degraded" // failed partway; the host is running a mix )
Release statuses recorded in the ledger.
The distinction between failed and degraded is the whole point of recording a status at all: one means the host is exactly as it was, the other means it is not, and an operator reading `shunt status` after a bad night needs to know which without inferring it from an error message.
const ( KindStep = "step" // work starting; shown only in verbose mode KindOK = "ok" // work completed KindInfo = "info" // noteworthy but not a step outcome KindLog = "log" // passthrough output from a container KindFail = "fail" // the operation failed; message explains why KindResult = "result" // terminal summary of a successful operation )
Event kinds. Constants rather than bare strings so the two binaries cannot drift on a typo that would silently stop rendering a whole class of event.
const DefaultRetain = 5
DefaultRetain is how many restorable releases a host keeps when the manifest does not say. Both ends fall back to it, so a spec that predates the field prunes the same way a current one does.
const Protocol = 1
Protocol is bumped when Spec or Event change shape incompatibly. The helper refuses a Spec whose protocol it does not understand, which turns a silent version-skew misdeploy into a clean error.
const SecretMountPath = "/run/secrets"
SecretMountPath is where a file-mode secrets directory is mounted. Fixed rather than configurable: it is the same path Docker Swarm and Kubernetes use, so an app written for either already looks in the right place.
Variables ¶
This section is empty.
Functions ¶
func HashSecret ¶
HashSecret is the one-way form a secret value takes once it is written to the host's ledger. Both ends use it: the helper redacts with it before persisting, and the CLI applies it to freshly-resolved values so `shunt plan` can compare like with like without a plaintext secret ever crossing back.
Keyed by a per-project salt rather than a bare digest. Plenty of real secrets are low-entropy or drawn from a known format — a six-digit pin, a postcode, an api key with a fixed prefix — and an unsalted truncated sha256 of those is recoverable by anyone who reads the ledger. The salt makes the stored digests useless off that host while still comparing equal for equal values.
func HashService ¶ added in v0.1.2
HashService fingerprints a service or accessory definition. encoding/json sorts map keys, so the same definition always produces the same digest.
func HealthProbePath ¶ added in v0.1.2
HealthProbePath is the path a reverse proxy can poll to decide whether a container should receive traffic.
A bare path is used directly. An absolute url is reduced to its path, because the proxy reaches the container on the deploy network and the host and port written for shunt's own probe do not apply there — previously such a service got no readiness gate at all purely because of how its health url happened to be spelled.
A command health check yields nothing: there is no way to express "run this inside the container" to Traefik or caddy. Those services fall back to the retry middleware, which covers a backend that is not yet listening but not one that is listening and still warming up.
func ProxyGatesReadiness ¶ added in v0.1.2
ProxyGatesReadiness reports whether the proxy itself can keep this service's container out of rotation until it is ready. False means the overlap leans on retry alone, which is worth saying out loud rather than leaving implicit.
func ScopeDigest ¶ added in v0.1.2
ScopeDigest names the subset of a release's secrets a service asked for.
Both ends derive host paths from it — the helper to write a scoped env-file or secrets directory, the CLI to point `shunt run` at the same one — so it has to be one function. Two implementations that disagreed by a sort order would send a console session to a directory that does not exist, and it would simply start with no secrets rather than fail.
func TreeSummary ¶ added in v0.1.2
TreeSummary totals a directory's regular files and reports the newest mtime among them. It defines what an Artifact's Bytes and MTime mean, so both ends must call it: two implementations differing by so much as a directory inode would make every directory artifact differ from itself on every deploy.
Not a hash, deliberately — that reads every byte on both sides, which is most of the cost of shipping the tree. Size and mtime are what rsync itself uses.
Types ¶
type Artifact ¶ added in v0.1.2
type Artifact struct {
Name string `json:"name"`
Dest string `json:"dest"`
Staged string `json:"staged"` // where the CLI rsync'd it; renamed onto Dest
Magic string `json:"magic,omitempty"`
Retain int `json:"retain"`
Bytes int64 `json:"bytes"`
MTime int64 `json:"mtime"` // unix seconds; rsync --times preserves it
// Dir marks an artifact that is a directory tree rather than a single file.
// The swap is the same rename, because renaming a directory within its
// parent is just as atomic as renaming a file.
Dir bool `json:"dir,omitempty"`
}
Artifact is one file to swap into place on the host.
type Entry ¶
type Entry struct {
ID string `json:"id"`
Status string `json:"status"`
StartedAt time.Time `json:"started_at"`
FinishedAt time.Time `json:"finished_at,omitempty"`
Images map[string]ImageRef `json:"images"`
Services []string `json:"services"`
Error string `json:"error,omitempty"`
// Spec is retained so a rollback can re-apply the exact previous release
// without needing the manifest that produced it.
Spec *Spec `json:"spec,omitempty"`
// Provenance is lifted out of the spec so `status` and the JSON contract can
// read it without unpacking a whole release.
Provenance Provenance `json:"provenance,omitzero"`
}
func (*Entry) Healthy ¶ added in v0.1.2
Healthy reports whether an entry represents a release that took over cleanly. Failed and degraded releases are neither serving nor safe to roll onto.
func (*Entry) Restorable ¶ added in v0.1.2
Restorable reports whether this entry can serve as a rollback target: it reached a healthy state, and it retained the spec needed to replay it.
type Event ¶
type Event struct {
Kind string `json:"kind"`
Step string `json:"step,omitempty"`
Message string `json:"message,omitempty"`
// Result payload, set when Kind == KindResult.
Release string `json:"release,omitempty"`
Status string `json:"status,omitempty"`
}
Event is one NDJSON line emitted by the helper on stdout. The CLI renders these; anything the helper writes to stderr is passed through as raw output.
type ImageRef ¶
type ImageRef struct {
// Ref is the tag the helper applies locally after load, e.g. shunt/latent-app:20260726-175612.
Ref string `json:"ref"`
// Digest is the OCI manifest digest exported by buildx.
Digest string `json:"digest"`
// External images are pulled on the host instead of loaded from the store.
External bool `json:"external"`
}
type Ledger ¶
type Ledger struct {
Project string `json:"project"`
// Current is the release believed to be *serving*. A deploy that fails
// before replacing any running container does not move it — the previous
// release is still up, and reporting otherwise would contradict the error
// the operator was just shown.
Current string `json:"current"`
// LastAttempt is the most recent deploy regardless of outcome. It differs
// from Current exactly when the last deploy failed without taking over.
LastAttempt string `json:"last_attempt,omitempty"`
// Accessories records the definition hash of each accessory as it was
// actually applied — when its container was created or recreated — keyed by
// name.
//
// It has to be tracked separately from the release spec because a deploy
// records the manifest it was *given*, not the accessory state it *applied*:
// `up` deliberately leaves an existing accessory alone. Diffing the manifest
// against the last recorded spec therefore made drift disappear after any
// unrelated deploy, while the container kept running the old config.
Accessories map[string]string `json:"accessories,omitempty"`
// Salt keys the secret hashes stored in this ledger. Generated once per
// project on the host, so a truncated digest of a low-entropy value is not
// brute-forceable from the ledger alone.
Salt string `json:"salt,omitempty"`
Releases []Entry `json:"releases"`
}
Ledger is the host-side record of what has been deployed. It lives at <root>/<project>/releases.json and is the authority for status and rollback.
func (*Ledger) KeepIDs ¶ added in v0.1.2
KeepIDs returns the releases whose images and env-files must survive pruning: the newest `retain` restorable ones, plus whatever is currently active.
Counting failed attempts toward `retain` is the subtle way to lose a rollback. A run of failed deploys — exactly the situation where you most want to go back — would otherwise push the last good release out of the keep set, and the next successful deploy would delete its images and its env-file. So failures are skipped rather than counted, and the history stays as deep as it claims.
func (*Ledger) Previous ¶
Previous returns the most recent successfully-activated release before the current one — the target of `shunt rollback` with no argument.
func (*Ledger) RecordAccessory ¶ added in v0.1.2
RecordAccessory notes the definition an accessory container was created with.
func (*Ledger) Trim ¶ added in v0.1.2
Trim bounds the ledger's length without dropping releases that are still rollback targets.
A plain "keep the last N entries" would let a run of failed deploys evict the last good release from the history altogether, which is the same bug KeepIDs exists to prevent, one layer up.
type Provenance ¶ added in v0.1.2
type Provenance struct {
Commit string `json:"commit,omitempty"`
Short string `json:"short,omitempty"`
Branch string `json:"branch,omitempty"`
Dirty bool `json:"dirty,omitempty"`
CLI string `json:"cli,omitempty"`
Deployer string `json:"deployer,omitempty"`
}
Provenance is the origin of a release: which commit, built by whom, with which shunt. Every field is best-effort — a project deployed from a tarball has no git metadata and must still deploy.
func (Provenance) Describe ¶ added in v0.1.2
func (p Provenance) Describe() string
Describe renders provenance for a human, or "" when nothing is known.
type Proxy ¶
type Proxy struct {
Kind string `json:"kind"`
Host string `json:"host"`
Path string `json:"path,omitempty"`
Port int `json:"port"`
EntryPoints []string `json:"entrypoints,omitempty"`
Retry int `json:"retry,omitempty"`
}
Proxy carries what the helper needs to emit discovery labels for an external reverse proxy.
type Service ¶
type Service struct {
Image string `json:"image"`
Command []string `json:"command,omitempty"`
Env map[string]string `json:"env,omitempty"`
Publish []string `json:"publish,omitempty"`
Volumes []string `json:"volumes,omitempty"`
Restart string `json:"restart"`
Health *Health `json:"health,omitempty"`
Requires []string `json:"requires,omitempty"`
Expose int `json:"expose,omitempty"`
Drain string `json:"drain,omitempty"`
Proxy *Proxy `json:"proxy,omitempty"`
// Secrets narrows which of the release's secrets this service receives.
// Empty means all of them.
Secrets []string `json:"secrets,omitempty"`
}
type Spec ¶
type Spec struct {
Protocol int `json:"protocol"`
Project string `json:"project"`
ID string `json:"id"` // release id, e.g. 20260726-175612-a1b2c3
Network string `json:"network"`
Retain int `json:"retain"`
// StorePath is the OCI layout directory on the host that the CLI rsync'd
// into before invoking the helper.
StorePath string `json:"store_path"`
// Images maps a manifest image name to the digest that must be present after
// load. The helper verifies this rather than trusting the transfer.
Images map[string]ImageRef `json:"images"`
// Accessories are ensured (created only if absent) before stages run, so a
// migration has its database available. They are never recreated by a normal
// deploy — see `shunt boot`.
Accessories map[string]Service `json:"accessories,omitempty"`
AccessoryOrder []string `json:"accessory_order,omitempty"`
// Artifacts have already been transferred to their staged path by the time
// the helper runs; it validates and swaps them.
Artifacts []Artifact `json:"artifacts,omitempty"`
Stages []Stage `json:"stages"`
Services map[string]Service `json:"services"`
Order []string `json:"order"` // service start order, precomputed by the CLI
// Secrets are applied to every service and stage on the host, either as an
// --env-file or as files under /run/secrets. See SecretMode.
Secrets map[string]string `json:"secrets,omitempty"`
// SecretMode is "env" (default) or "file". In file mode each secret is
// written to its own 0600 file in a directory mounted read-only at
// /run/secrets, which keeps the values out of `docker inspect`.
SecretMode string `json:"secret_mode,omitempty"`
// Provenance records where this release came from. It is carried on the
// spec so the host can store it with the release, which is what lets
// `shunt status` answer "which commit is production running" without the
// operator holding a mapping in their head.
Provenance Provenance `json:"provenance,omitzero"`
// RollbackOnFailure asks the helper to restore the previous release if this
// one fails *after* replacing a running container. Opt-in: rolling back
// automatically is right for a stateless web app and wrong for a deploy
// whose stages already migrated a database.
RollbackOnFailure bool `json:"rollback_on_failure,omitempty"`
// ExpectedCurrent is the release the host was serving when this plan was
// built. The helper refuses to apply a spec whose assumption no longer
// holds, so a plan computed against one state cannot be applied to another.
// Empty means "no expectation" — a first deploy, or a caller that did not
// read the host first.
ExpectedCurrent string `json:"expected_current,omitempty"`
}
Spec is a complete, self-contained description of one release. Everything the helper needs is here; it never reads the manifest and never phones home.
func (*Spec) SecretsAsFiles ¶ added in v0.1.2
SecretsAsFiles reports whether this release delivers secrets as files.