Documentation
¶
Overview ¶
Package spec defines the AppSpec: the sole record of how an app runs.
Nothing is read from the repo at deploy time (R-020). Specs are immutable and versioned; editing produces a new revision and rollback is repointing (R-152). A spec carries no policy, no grants, and no secret values, which is what makes export safe to hand to someone. See design 01.
Index ¶
- Constants
- Variables
- func Address(host, slug string, r Routing) string
- func AliasProblem(alias string) string
- func BuildInputs(s *AppSpec) string
- func CheckPathPrefix(p string) *errs.Error
- func CheckSlotValue(s *AppSpec, slot, value string) error
- func CheckSourceURL(src Source) error
- func CheckUploadOwner(src Source, appID string) error
- func ConfineStoredPaths(s *AppSpec)
- func DropStalePin(prev, next *AppSpec)
- func EnvSlot(s *AppSpec, workload, key string) (string, bool)
- func FillSlotLiteral(s *AppSpec, slot, secretRef string) bool
- func FillSlotValue(s *AppSpec, slot, value string) bool
- func LimitsChanged(prev, next *AppSpec) bool
- func MarkSlotOptional(s *AppSpec, slot string) bool
- func MountPathProblem(p string) string
- func NormalizePathPrefix(p string) string
- func ReadsSecret(s *AppSpec, key string) bool
- func ReservedName(name string) bool
- func SetEnv(s *AppSpec, workload, key string, entry EnvEntry)
- func SetHostname(s *AppSpec, hostname string) error
- func SlotPartOf(value string, part SlotPart) (string, bool)
- func SlotSecretKey(slot string) string
- func ValidHostname(h string) bool
- func Validate(s *AppSpec) error
- type AppSpec
- type AutoDeploy
- type AutoDeployTrigger
- type Availability
- type Build
- type BuildStrategy
- type Change
- type Class
- type Defaults
- type Deploy
- type DeployStrategy
- type Diff
- type Egress
- type EgressMode
- type EnvEntry
- type EnvSource
- type File
- type Health
- type HealthSource
- type Healthcheck
- type IsolationClass
- type KV
- type ModeSource
- type Mount
- type Origin
- type Port
- type PortSource
- type Resolution
- type ResolutionMode
- type ResourceLimits
- type Resources
- type Retention
- type Routing
- type RoutingMode
- type RuntimeRef
- type SecretFormat
- type Slot
- type SlotPart
- type SlotType
- type Source
- type SourceType
- type StandbyMode
- type Volume
- type VolumeSource
- type Warning
- type Workload
- type WorkloadBuild
Constants ¶
const ( WarnNoPersistentVolume = "WARN_NO_PERSISTENT_VOLUME" WarnPathRoutingIncompatible = "WARN_PATH_ROUTING_INCOMPATIBLE" WarnComposeConstructRewritten = "WARN_COMPOSE_CONSTRUCT_REWRITTEN" WarnUndeclaredDependency = "WARN_UNDECLARED_DEPENDENCY_SUSPECTED" // WarnPrimaryWorkloadAssumed is R-026 with the question unanswered: a // compose file with several services does not say which one a person opens // in a browser, so Pando picks the likeliest and says so rather than // producing a spec that cannot deploy. WarnPrimaryWorkloadAssumed = "WARN_PRIMARY_WORKLOAD_ASSUMED" // WarnScreeningAdvisory is the one code an AI screener may emit, and it is // not its choice (design 10 §3). The codes above mean specific things that // specific parts of Pando produced; a screener able to pick one could claim // the compose importer rewrote something it never touched. WarnScreeningAdvisory = "WARN_SCREENING_ADVISORY" )
Warning codes that must exist (design 01 §2.8).
const FileSizeLimit = 64 << 10
FileSizeLimit caps one carried file. Config files are kilobytes; the cap exists so that a spec stays something a person can read and a database row stays a database row.
const SchemaVersion = 1
SchemaVersion is the current AppSpec wire version.
Stored alongside every revision so migrating specs across Pando releases is mechanical rather than archaeological.
Variables ¶
var PostgresSchemes = []string{"postgresql", "postgres"}
PostgresSchemes are the spellings a Postgres slot's Scheme may take. The first is the default: postgresql://, the one libpq documents.
var ReservedPaths = []string{
"api", "admin", "assets", "login", "index.html", "healthz", "readyz", ".well-known", ".pando",
}
ReservedPaths are the first path segments Pando answers itself, on every hostname that is not an app's own. An app on a path cannot take one: the router serves these before the proxy sees the request, so the app would never receive them — and a prefix that shadowed /login or /api would be a page on Pando's own origin that looks like Pando.
The router's own routes are checked against this list by a test (httpapi TestR167_EveryPandoPathIsReserved), so adding a top-level route without reserving it fails the build.
Functions ¶
func Address ¶
Address renders where an app is reached, for showing to a person.
One implementation, because "where is this app" has one answer and three ways to get it wrong. Both of the console's screens used to assemble "/" + slug, which is the path-mode answer handed to every app whatever mode it was in — so on a loopback install, where every app is port mode, both screens pointed at an address that belonged to nobody. R-261: this is the API's answer, not a rule each client re-derives.
host is the authority the caller reached Pando on, including its port. Port mode needs it because a port is not an address on its own, and the only host this process can honestly name is the one the request came in on — anything else is a guess about DNS, and Pando is not the only thing that might be in front of it.
func AliasProblem ¶ added in v0.14.0
AliasProblem is why a network alias cannot be used, or empty when it can.
func BuildInputs ¶ added in v0.5.0
BuildInputs is a digest of everything in a spec that decides what a build produces, and nothing else: where the source is read from, how it is built, with which arguments and under which constraints, by which builder, for which runtime, and each workload's own build. A spec whose BuildInputs are unchanged builds the same thing from the same commit.
It is how a deploy knows the image detection built is the image it would build (R-311, design 09 §4.1): same source, same inputs, so the image and its scan are reused rather than built a second time. Anything else in the spec — ports, environment, routing, limits — changes how the image runs, not what it is, and is left out so that answering a port does not cost a rebuild.
The commit is not part of it: the caller compares the checkout it builds from, which names an upload by its digest as well as a repository by its commit.
func CheckPathPrefix ¶
CheckPathPrefix refuses a path an app cannot be served under, saying why.
func CheckSlotValue ¶ added in v0.5.0
CheckSlotValue refuses a value for a slot whose format it does not fit. A slot with no format, or no slot by that key, accepts anything: the value is the person's answer, and Pando has nothing to check it against.
func CheckSourceURL ¶ added in v0.8.0
CheckSourceURL refuses a repository address Pando will not fetch, said when a person gives it rather than when detection fails (R-092; 2026-10-16 assessment, finding 3).
https, http and ssh are fetched, including git's git@host:path shorthand. Anything else — file://, a bare path on the machine Pando runs on, git:// — is refused, and the fetch refuses it too (source/transport.go). An address carrying a password or token is refused as well: the spec is append-only and exported (R-152), so a credential in it can never be taken back out, and Pando keeps credentials in source connections, encrypted (R-190).
Where an address resolves to is not checked here: that depends on DNS when it is fetched, and the fetch checks every address it dials.
func CheckUploadOwner ¶ added in v0.14.0
CheckUploadOwner refuses an uploaded source that names an upload other than the app's own. An app's upload is stored under its own ID (design 04 §4), and naming another would deploy what was uploaded to that app.
func ConfineStoredPaths ¶ added in v0.14.0
func ConfineStoredPaths(s *AppSpec)
ConfineStoredPaths rewrites, in a spec read back from storage, a subdirectory or build context that is absolute or climbs above the repository as the place inside the repository a build was always given for it (R-020).
Before validation refused such paths, the source view and the builder each read them rooted where they started: an absolute path as one under the directory, and ".." as no higher than it. A revision written then deployed from there, and rewriting it the same way keeps it deploying — on a redeploy, a rollback or a pin — instead of failing the stricter validation that new specs meet. A path inside the repository is left exactly as it is. A Dockerfile is not rewritten: one that named a place outside the repository never built from the repository.
func DropStalePin ¶
func DropStalePin(prev, next *AppSpec)
DropStalePin clears an image app's digest when next names a different image than prev and still carries prev's digest (issue #41).
A digest pins what an image reference resolved to, so it is only true of the reference it was resolved from. Someone who edits ghcr.io/acme/web:1 to :2 and leaves the digest would otherwise go on running :1 — the pull is by digest — with a spec that says :2. Cleared rather than refused: the next deploy pins :2 the way it pins any unpinned image, in a revision of its own. A digest that was changed alongside the image is left alone; that is a pin somebody chose.
func EnvSlot ¶
EnvSlot returns the slot a variable is filled from, when it is: the SlotRef on the entry with this key, on the named workload or, with none named, on any workload.
func FillSlotLiteral ¶
FillSlotLiteral fills a slot with a value somebody supplied, stored as the secret named secretRef (R-132).
A variable filled from a slot was given a value by replacing its entry, which dropped the SlotRef and left the slot itself unfilled — and a required unfilled slot refuses the deploy, so a value typed during review did nothing but move the refusal to deploy time. The value belongs on the slot. A service slot filled this way stops being provisioned: the person has said what the app should connect to.
func FillSlotValue ¶
FillSlotValue fills a slot with a value somebody chose not to keep secret: it is stored in the spec as the slot's target, which is what the variable is set to at deploy (a bound resolution). A URL or a domain name is not a secret, and forcing every slot value into one hid a value somebody wanted to read back.
func LimitsChanged ¶ added in v0.15.0
LimitsChanged reports whether next sets resource limits prev does not, as stored: the app's own (CPU, memory, disk and whether they override the host's defaults), or any workload's — a limit given, taken away or changed on a workload prev has, or one given to a workload prev does not have. Changing any of them needs app.resources.override (R-241, R-241a). A workload that goes, with its limit, lets nothing run larger, so it is not a change here. A workload's resources left out and set to nothing are the same (WorkloadLimits reads both as the app's). prev nil is a spec with no limits of its own.
func MarkSlotOptional ¶
MarkSlotOptional records that the app runs without a slot, because a person reviewing it said so: an optional slot nobody fills leaves its variable unset rather than refusing the deploy (R-132). Detection marks slots required from a crash or a template, and it can be wrong.
func MountPathProblem ¶ added in v0.14.0
MountPathProblem is why a mount path cannot be used, or empty when it can.
func NormalizePathPrefix ¶
NormalizePathPrefix is a path as Pando stores it: one leading slash, no trailing one, lowercase.
func ReadsSecret ¶ added in v0.13.0
ReadsSecret reports whether the spec puts a stored secret into a workload: an env entry that references it, or a slot filled with it.
A secret is stored apart from the spec, and only a reference in the spec brings it into the app. One that nothing references is stored and never read — setting it and deploying changes nothing (R-193 covers a secret the app reads, not one it does not).
func ReservedName ¶ added in v0.14.0
ReservedName reports whether a workload name or alias is in the space Pando names its own containers and networks in ("pando-<app>-<workload>", "pando-egress"). Pando's own container shares every app's network, so an app answering to one of those names could receive traffic meant for another.
func SetEnv ¶
SetEnv sets a variable a person chose, on the workloads it belongs to.
With a workload named, that workload only. Without one, every workload that already declares the key — a variable detection found in .env.example is usually read by one part, and naming the part is something the person should not have to know. A key no workload declares goes to the primary one: it is a variable the person is adding, and the primary workload is the app.
The entry replaces any existing one with the key, and is marked as the person's (EnvFromUser), so it outlives a re-detection (R-022).
func SetHostname ¶
SetHostname sets the address an app is served at, in subdomain mode — the one mode where the address is a name somebody chooses rather than a port Pando allocates or a prefix derived from the app's slug. Refused in any other mode, with the reason, rather than stored where nothing reads it.
func SlotPartOf ¶ added in v0.5.0
SlotPartOf reads one part out of a connection string. The whole string for an empty part; for anything else the URL must parse, and ok is false when it does not — a value somebody pasted that is not a URL has no host to take.
func SlotSecretKey ¶
SlotSecretKey is the secret a literal slot value is stored under. The same name PUT /slots/{key} writes, so a value set during review and one set later on the settings screen are one value.
func ValidHostname ¶ added in v0.14.0
ValidHostname reports whether h is a DNS name, with or without the dot that ends a fully qualified one.
func Validate ¶
Validate checks a spec's internal consistency.
Run on every spec before it is pinned, and again at plan time (design 01 §3). Everything here is answerable from the spec alone — rules needing adapters or policy belong to the planner, which is phase 3, and are deliberately absent so that validation can run without any of that being configured.
Errors accumulate rather than short-circuiting. Someone hand-writing a spec deserves every problem at once, not one per round trip.
Types ¶
type AppSpec ¶
type AppSpec struct {
SchemaVersion int `json:"schema_version"`
AppID string `json:"app_id"`
Revision int `json:"revision"`
CreatedAt time.Time `json:"created_at"`
CreatedBy string `json:"created_by"`
Origin Origin `json:"origin"`
Source Source `json:"source"`
Build Build `json:"build"`
Workloads []Workload `json:"workloads"`
Volumes []Volume `json:"volumes"`
Slots []Slot `json:"slots"`
Routing Routing `json:"routing"`
Runtime RuntimeRef `json:"runtime"`
Deploy Deploy `json:"deploy"`
Health Health `json:"health"`
Resources Resources `json:"resources"`
Egress Egress `json:"egress"`
Retention Retention `json:"retention"`
// Availability is how the app is kept running when its host is lost
// (R-419, R-422). Left out of an app that says nothing, which is most.
Availability Availability `json:"availability,omitzero"`
// Warnings are carried, not resolved. A warning is never a blocker —
// blockers are PLAN_* errors.
Warnings []Warning `json:"warnings,omitempty"`
}
AppSpec is the sole record of how an app runs.
Nothing is read from the repo at deploy time (R-020). If it is not here, it does not happen. Three things are deliberately absent:
- Policy, which is evaluated live at plan time so R-274 works.
- Grants, which live separately so sharing does not create a revision.
- Secret values, which makes export safe to hand to someone (R-020).
Adapters are named by reference, never by vocabulary (R-251): the spec says runtime: {adapter_ref: "rt_docker_local"}, never Docker arguments.
func Carry ¶
Carry returns next with the decisions a person made in pinned preserved.
pinned may be nil, which is the first acceptance: there is nothing to carry and the proposal stands as it is.
func (*AppSpec) NamesImage ¶ added in v0.14.0
NamesImage reports whether ref is an image the spec itself names — an image app's image, or the published one a workload without a build runs — rather than one an app's build produced. Compared by repository, so a tag or digest the deploy pinned it to is the same image. The install registry's credential is for an app's own builds only: an author who writes down another app's build gets no credential to pull it with.
func (*AppSpec) PrimaryWorkload ¶
PrimaryWorkload returns the workload the app's URL resolves to.
func (*AppSpec) WorkloadLimits ¶ added in v0.14.0
func (s *AppSpec) WorkloadLimits(w Workload) ResourceLimits
WorkloadLimits is what a workload runs limited to (R-240): its own CPU and memory where it sets them, the app's where it does not, and never more than the app's. A runtime reads zero as no limit at all, so a workload that set its resources to nothing would otherwise run unbounded.
type AutoDeploy ¶
type AutoDeploy struct {
Enabled bool `json:"enabled"`
Trigger AutoDeployTrigger `json:"trigger,omitempty"`
// Branch is what the branch trigger follows. Empty follows Source.Ref,
// the ref the app was deployed from.
Branch string `json:"branch,omitempty"`
// TagPattern is what the release trigger counts as a release: a glob
// matched against tag names (`release-*`). Empty means a stable semantic
// version, v1.2.3 (O-58).
TagPattern string `json:"tag_pattern,omitempty"`
}
AutoDeploy configures automatic deploys. Off by default (R-141).
type AutoDeployTrigger ¶
type AutoDeployTrigger string
AutoDeployTrigger is what causes an automatic deploy.
const ( TriggerBranchUpdated AutoDeployTrigger = "branch_updated" TriggerReleaseTagged AutoDeployTrigger = "release_tagged" )
type Availability ¶ added in v0.17.0
type Availability struct {
// Standby overrides host policy's standby for this app (R-419). How an
// app runs is the spec's (R-020), so the override is here rather than a
// setting beside it.
Standby StandbyMode `json:"standby,omitempty"`
// Priority orders the apps a host starts at failover when host policy
// allow_failover_oversubscription let their standbys be placed past what
// it keeps free: higher first, then by app. 0 by default; negative is
// allowed, for an app that should start last (R-422).
Priority int `json:"priority,omitempty"`
}
Availability is how an app is kept running when its host is lost.
func (Availability) WantsStandby ¶ added in v0.17.0
func (a Availability) WantsStandby(policyDefault bool) bool
WantsStandby is whether the app keeps a standby, given host policy's default.
type Build ¶
type Build struct {
Strategy BuildStrategy `json:"strategy"`
AdapterRef string `json:"adapter_ref"`
Dockerfile string `json:"dockerfile,omitempty"`
Context string `json:"context,omitempty"`
Target string `json:"target,omitempty"`
ComposeFile string `json:"compose_file,omitempty"`
StaticDir string `json:"static_dir,omitempty"`
// GeneratedFiles are build inputs Pando produced rather than found, keyed
// by path relative to the build context. Written into the checkout before
// the build and never into anybody's repository.
//
// Opaque to core, which stores and replays them and interprets nothing:
// only the builder that produced them knows what they mean (R-251). Today
// that is a Railpack plan, or a Dockerfile Pando wrote and the files it
// references; a spec stored before issue #172 carries a nixpacks plan.
//
// They live in the spec because R-020 makes the state store the sole record
// of how an app runs, and a plan regenerated at each build is not a record:
// the same commit would build differently after the planner was upgraded.
// Storing them pins the build to what somebody reviewed, and makes it
// something they can edit without putting deployment files in their repo —
// which is the other half of R-020.
//
// Append-only like the rest of the spec (R-152), so an edit is a revision
// that can be diffed and rolled back.
GeneratedFiles map[string]string `json:"generated_files,omitempty"`
// Args are build arguments and are stored in the clear. Anything sensitive
// is a slot — this is why there is no secret-bearing build arg.
Args []KV `json:"args,omitempty"`
// IsolationFloor is deliberately separate from Runtime.IsolationFloor
// (R-114): how isolated a build must be is a different question from how
// isolated the running app must be.
IsolationFloor IsolationClass `json:"isolation_floor"`
TimeoutSeconds int `json:"timeout_seconds"`
EgressMode EgressMode `json:"egress_mode"`
EgressAllow []string `json:"egress_allow,omitempty"`
}
Build describes how the app is built.
type BuildStrategy ¶
type BuildStrategy string
BuildStrategy is how an app is built.
const ( BuildDockerfile BuildStrategy = "dockerfile" BuildCompose BuildStrategy = "compose" BuildBuildpack BuildStrategy = "buildpack" BuildStatic BuildStrategy = "static" BuildPrebuilt BuildStrategy = "prebuilt" )
type Change ¶
type Change struct {
Class Class `json:"class"`
Path string `json:"path"`
// Summary is written for the person confirming the change, not for a log.
Summary string `json:"summary"`
From any `json:"from,omitempty"`
To any `json:"to,omitempty"`
}
Change is one difference between two specs.
type Class ¶
type Class string
Class is how consequential a change is.
The classification drives the UI treatment (design 01 §4): benign collapses, restart and rebuild are shown, and destructive requires explicit confirmation. Re-detection, rollback review, and promoting a compose service all present the same diff through the same machinery.
const ( // Benign is display-only: retention, warnings. Benign Class = "benign" // Restart requires the workloads to come back: env, resources, health. Restart Class = "restart" // Rebuild triggers a build: source, build configuration. Rebuild Class = "rebuild" // Destructive can lose data or break access, and requires explicit // confirmation: a volume removed, a slot moved from provisioned to bound, a // routing mode changed, a runtime adapter changed (R-257). Destructive Class = "destructive" )
type Defaults ¶
type Defaults struct {
// Adapters. Empty means the install has none of that category configured,
// and the planner's PLAN_ADAPTER_NOT_CONFIGURED says so far better than
// anything this could invent.
RoutingAdapter string
RuntimeAdapter string
BuilderAdapter string
// RoutingMode is the routing adapter's declared default (R-162): adding an
// app uses it without asking.
RoutingMode RoutingMode
// BaseDomain is what a subdomain is carved out of. Only consulted in
// subdomain mode.
BaseDomain string
// Isolation floors from host policy (R-024, R-114).
BuildIsolation IsolationClass
RuntimeIsolation IsolationClass
BuildTimeoutSeconds int
// Resources are the host defaults every new app inherits (R-240).
Resources Resources
Retention Retention
}
Defaults are the install's answers to everything a repository cannot say about itself.
R-104 draws the line this implements: "Questions are blockers; everything else is configuration. Anything with a reasonable default gets the default and is changeable later in settings." A repository can say it builds from a Dockerfile and listens on 3000. It cannot say which runtime adapter this install uses, how apps here are addressed, or what a memory limit should be — and none of those is a question worth putting to someone R-005 says may not know what a port is.
This lives in spec rather than in detect because detection is not the only producer of an incomplete spec. An imported one (R-152, OriginImported) has exactly the same gap, and so does a hand-written one that omits a section.
Plain values, no adapter types: api imports spec, so spec cannot import api. Whoever holds the registry resolves the refs and passes them in.
func StandardDefaults ¶
func StandardDefaults() Defaults
StandardDefaults are the [P] values from the requirements, for the parts of Defaults that do not come from adapters or host policy.
Each is a proposed default and may be overridden with a reason:
R-152 10 pinned spec revisions retained for rollback
R-211 daily backups, 7 retained
R-223 100 MB of logs per app, oldest discarded first
R-240 CPU, memory and disk are set at the host; these are the fallbacks
for an install that has not set them
func (Defaults) Apply ¶
Apply fills in every field the spec leaves empty.
It only ever fills gaps. A spec that already says how it is routed keeps saying it — this must be safe to run over a spec someone has edited, because it runs on every revision and not only the first.
slug names the app in an address. It is passed rather than read off the spec because a spec has an AppID and an AppID is not something to put in a URL.
type Deploy ¶
type Deploy struct {
Strategy DeployStrategy `json:"strategy"`
AutoDeploy AutoDeploy `json:"auto_deploy"`
// AutoRollback is off by default (R-147), because an app with no health
// signal is running rather than unhealthy, and rolling back on a signal
// that does not exist would be worse than leaving it alone.
AutoRollback bool `json:"auto_rollback"`
// RequireApproval is the app asking for a second pair of eyes on its own
// deploys (R-154). Read from the running spec as well as the one being
// deployed, so switching it off is itself approved. Host policy can require
// the same of an app without its owner being able to turn it off.
RequireApproval bool `json:"require_approval,omitempty"`
}
Deploy configures deployment behavior.
type DeployStrategy ¶
type DeployStrategy string
DeployStrategy is how a new version replaces the old.
const ( // DeployRecreate is the default (R-144). The app is down during the swap, // which is the accepted cost. DeployRecreate DeployStrategy = "recreate" // DeployStartThenSwap runs two copies at once and is opt-in only (R-145). DeployStartThenSwap DeployStrategy = "start_then_swap" )
type Diff ¶
type Diff struct {
Changes []Change `json:"changes"`
}
Diff is a classified comparison.
func (Diff) RequiresConfirmation ¶
RequiresConfirmation reports whether a human must explicitly accept this.
type Egress ¶
type Egress struct {
Mode EgressMode `json:"mode"`
List []string `json:"list,omitempty"`
Add []string `json:"add,omitempty"`
Remove []string `json:"remove,omitempty"`
BlockPrivate *bool `json:"block_private,omitempty"`
// Allowlist is the field Mode allowlist used before issue #79, when an
// app's list replaced the install's instead of narrowing it. Read as List
// by Normalize and never written.
Allowlist []string `json:"allowlist,omitempty"`
}
Egress is how an app's outbound access differs from the installation's (R-181 – R-185, design 01 §2.7).
The install's rules are the starting point, always. An app moves from there in two ways, which may be combined:
- Add and Remove edit the install's list for this app. Which of them loosens depends on the install's mode: adding to an allowlist loosens, adding to a denylist tightens, and removing is the reverse.
- Mode allowlist or denylist, with List, is a list of the app's own applied on top of the install's: a destination must pass both. It can only narrow, so it never needs policy's permission.
BlockPrivate nil keeps the install's switch; true turns it on (tightening); false turns it off, which loosens when the install has it on.
Loosening is gated by host policy (R-183) and by app.egress.loosen; everything else by app.egress.tighten (R-184). policy.Document.EgressFor resolves this against the installation and says which parts loosen.
func (*Egress) Normalize ¶
func (e *Egress) Normalize()
Normalize reads a spec written before issue #79 in today's terms. It never loosens: an old allowlist becomes the app's own list, which narrows, where it used to replace the install's.
func (Egress) Same ¶
Same reports whether two egress settings say the same thing once each is read in today's terms (Normalize). Whether a change needs app.egress.tighten or app.egress.loosen starts from this (R-184): a spec written before issue #79 that is saved again unchanged has not changed its egress.
type EgressMode ¶
type EgressMode string
EgressMode controls outbound network access.
const ( EgressAllowAll EgressMode = "allow_all" EgressDenylist EgressMode = "denylist" EgressAllowlist EgressMode = "allowlist" EgressInherit EgressMode = "inherit" // EgressBlockPrivate is a build egress mode (R-118). For an app it used to // be a mode too and is now a switch that works alongside any list // (Egress.BlockPrivate, R-181); a spec that still says it is read that way. EgressBlockPrivate EgressMode = "block_private" )
type EnvEntry ¶
type EnvEntry struct {
Key string `json:"key"`
Value *string `json:"value,omitempty"`
SlotRef *string `json:"slot_ref,omitempty"`
SecretRef *string `json:"secret_ref,omitempty"`
// SlotPart takes one piece of the slot's connection string rather than
// all of it, for an app that is configured the way libpq, Laravel and most
// official images are: PGHOST, PGUSER and PGPASSWORD, or DB_HOST and
// DB_PASSWORD, each its own variable. One slot fills them all, so a
// provisioned database is one answer however many variables it takes.
// Empty is the whole connection string. Set only with SlotRef.
SlotPart SlotPart `json:"slot_part,omitempty"`
// Source records where this entry came from, the way Port.Source and
// Volume.Declared already do.
//
// It decides what survives a re-detection. Detection proposes a whole spec,
// and accepting one used to replace the pinned spec entirely — so a
// variable somebody typed was gone the moment they re-detected after adding
// a Dockerfile, with the app still running and no sign anything had been
// dropped. Provenance is what lets the two be merged instead: an entry a
// person set outlives anything Pando worked out for itself.
Source EnvSource `json:"source,omitempty"`
}
EnvEntry is one environment variable. Exactly one of Value, SlotRef, or SecretRef is set — validation enforces it, because an entry with two sources has no defined resolution and an entry with none silently vanishes.
type EnvSource ¶
type EnvSource string
EnvSource records how an environment entry was determined.
const ( // EnvFromUser is set by a person, and survives re-detection. EnvFromUser EnvSource = "user" // EnvFromCompose was imported from a compose file, and is replaced when // that file is read again — the file is the record, and somebody editing it // expects the change to land. EnvFromCompose EnvSource = "compose" // EnvFromDetection was inferred. Empty means the same thing: every entry // predating this field came from detection or an import, never from a // person, because there was no way to add one by hand. EnvFromDetection EnvSource = "detected" // EnvFromScreening was set by an AI screener (R-334, design 10 §5). // // Deliberately not EnvFromUser. A screener is not a person: its value is // replaced by the next screening rather than carried across a re-detection, // which is what Carry already does for everything that is not user-sourced. EnvFromScreening EnvSource = "screened" )
type File ¶
type File struct {
Path string `json:"path"`
// Content is stored in the clear and appears in an exported spec. It comes
// out of the repository, so it is already as public as the repository —
// anything sensitive belongs in a secret or a slot, not in a config file
// somebody committed.
Content string `json:"content"`
// Mode is the file's permission bits, defaulting to 0644. Set for the
// entrypoint script somebody mounts and expects to be executable.
Mode int `json:"mode,omitempty"`
}
File is a configuration file placed in a workload at start.
A Caddyfile, an nginx.conf, a prometheus.yml: the small text file a compose service bind-mounts from the repository. Storage Pando manages is a directory and cannot stand in for a file, and R-020 forbids reading the repository at deploy time, so the file itself is carried here — read once at detection, pinned to the revision, replayed on every start.
The same reasoning as Build.GeneratedFiles, one layer along: the spec is the sole record of how an app runs, and a file fetched from a branch at deploy time would make the same revision deploy differently tomorrow. Because it is a snapshot, changing the file in the repository does nothing until somebody re-detects — which is a real limit, and is what the import warning says.
Content is text and capped at FileSizeLimit. This is not a way to ship an application: a file big enough to matter is a build input, and builds have their own path.
type Health ¶
type Health struct {
Source HealthSource `json:"source"`
Path string `json:"path,omitempty"`
Port int `json:"port,omitempty"`
IntervalSeconds int `json:"interval_seconds"`
TimeoutSeconds int `json:"timeout_seconds"`
Retries int `json:"retries"`
}
Health configures health checking.
type HealthSource ¶
type HealthSource string
HealthSource is where a health signal comes from, in precedence order (R-221).
const ( HealthFromCompose HealthSource = "compose" HealthFromHTTP HealthSource = "http" HealthFromTCP HealthSource = "tcp" HealthFromProcess HealthSource = "process" )
type Healthcheck ¶
type Healthcheck struct {
Command []string `json:"command,omitempty"`
Path string `json:"path,omitempty"`
Port int `json:"port,omitempty"`
IntervalSeconds int `json:"interval_seconds,omitempty"`
TimeoutSeconds int `json:"timeout_seconds,omitempty"`
Retries int `json:"retries,omitempty"`
}
Healthcheck is a per-workload override.
type IsolationClass ¶
type IsolationClass int
IsolationClass is ordered so policy floors can be compared (R-114, R-255). Gaps of 10 leave room to insert classes without a migration.
const ( IsolationContainer IsolationClass = 10 IsolationSandboxed IsolationClass = 20 IsolationVM IsolationClass = 30 IsolationDedicatedHost IsolationClass = 40 )
type ModeSource ¶
type ModeSource string
ModeSource records whether a routing mode was inherited or chosen. Relevant when host policy later restricts overrides (R-274, O-10).
const ( ModeFromAdapterDefault ModeSource = "adapter_default" ModeFromUserOverride ModeSource = "user_override" )
type Mount ¶
type Mount struct {
VolumeID string `json:"volume_id"`
Path string `json:"path"`
ReadOnly bool `json:"read_only"`
}
Mount attaches a volume to a path.
type Origin ¶
type Origin string
Origin records how a revision came to be.
const ( OriginDetected Origin = "detected" OriginEdited Origin = "edited" OriginRedetect Origin = "redetected" OriginImported Origin = "imported" OriginManual Origin = "manual" // OriginAutoDeploy is a revision auto-deploy cut because the branch or // release it watches moved (R-141): the previous revision with a new // commit and nothing else changed. OriginAutoDeploy Origin = "auto_deploy" )
type Port ¶
type Port struct {
Number int `json:"number"`
Protocol string `json:"protocol"`
Source PortSource `json:"source"`
}
Port is a port a workload listens on.
type PortSource ¶
type PortSource string
PortSource records how a port was determined.
Retained because the review UI needs it: "we watched your app bind 3000" reads differently from "we guessed 3000 because it's a Next.js app", and the user reviewing the proposal deserves to know which.
const ( PortObserved PortSource = "observed" PortExpose PortSource = "expose" PortCompose PortSource = "compose" PortFramework PortSource = "framework" PortUser PortSource = "user" // PortScreened was supplied by an AI screener, and only where nothing was // observed. R-333: watching the process bind beats reading about it, so a // screener may fill this field and may never overwrite PortObserved. PortScreened PortSource = "screened" )
type Resolution ¶
type Resolution struct {
Mode ResolutionMode `json:"mode"`
ServiceRef string `json:"service_ref,omitempty"`
Target string `json:"target,omitempty"`
// SecretRef holds a literal value. A literal is stored as a secret rather
// than inline, which keeps "export is safe to hand to someone" true without
// a special case.
SecretRef string `json:"secret_ref,omitempty"`
}
Resolution fills a slot.
type ResolutionMode ¶
type ResolutionMode string
ResolutionMode is how a slot is filled (R-131).
const ( ResolutionProvisioned ResolutionMode = "provisioned" ResolutionBound ResolutionMode = "bound" ResolutionLiteral ResolutionMode = "literal" )
type ResourceLimits ¶
type ResourceLimits struct {
CPUMillis int `json:"cpu_millis,omitempty"`
MemoryBytes int64 `json:"memory_bytes,omitempty"`
}
ResourceLimits caps one workload.
func LimitWithin ¶ added in v0.14.0
func LimitWithin(own ResourceLimits, app Resources) ResourceLimits
LimitWithin is own, with each value that is unset or more than the app's replaced by the app's.
type Resources ¶
type Resources struct {
CPUMillis int `json:"cpu_millis"`
MemoryBytes int64 `json:"memory_bytes"`
DiskBytes int64 `json:"disk_bytes"`
// Overridden requires app.resources.override (R-241).
Overridden bool `json:"overridden"`
}
Resources are the app's limits, inherited from host defaults (R-240).
type Retention ¶
type Retention struct {
LogBytes int64 `json:"log_bytes"`
BackupDailyCount int `json:"backup_daily_count"`
SpecRevisions int `json:"spec_revisions"`
}
Retention caps what is kept.
type Routing ¶
type Routing struct {
AdapterRef string `json:"adapter_ref"`
Mode RoutingMode `json:"mode"`
ModeSource ModeSource `json:"mode_source"`
Hostname string `json:"hostname,omitempty"`
PathPrefix string `json:"path_prefix,omitempty"`
Port int `json:"port,omitempty"`
}
Routing places the app behind Pando's proxy.
type RoutingMode ¶
type RoutingMode string
RoutingMode is how traffic reaches the app.
const ( RoutingSubdomain RoutingMode = "subdomain" RoutingPath RoutingMode = "path" RoutingPort RoutingMode = "port" )
type RuntimeRef ¶
type RuntimeRef struct {
AdapterRef string `json:"adapter_ref"`
IsolationFloor IsolationClass `json:"isolation_floor"`
}
RuntimeRef names the runtime adapter and its isolation floor.
type SecretFormat ¶ added in v0.5.0
type SecretFormat string
SecretFormat is the shape a slot's value must take, for a slot that holds a key the app generates nothing for and cannot start without: Rails' SECRET_KEY_BASE, Laravel's APP_KEY, Django's SECRET_KEY.
It exists so that "generate a value" produces one the app accepts. A random string is the right answer for most secrets and the wrong one for these — Laravel refuses a key that is not 32 bytes ("Unsupported cipher or incorrect key length"), Phoenix one shorter than 64, and Django's deploy check one shorter than 50 — and a person who pressed Generate has no way to know which (R-130: a hole with a type).
Empty means the value has no shape Pando knows of, and anything goes.
const ( // FormatRandom is any non-empty string. Generated as 32 random bytes. FormatRandom SecretFormat = "random" // FormatRandomLong is at least 64 characters: Rails' and Phoenix's // SECRET_KEY_BASE. Generated as 64 random bytes. FormatRandomLong SecretFormat = "random_long" // FormatDjangoKey is what Django's deploy check accepts as SECRET_KEY: at // least 50 characters, at least 5 of them different, and not the // "django-insecure-" prefix startproject writes. FormatDjangoKey SecretFormat = "django_key" // FormatLaravelKey is a Laravel application key: "base64:" and 32 bytes // in base64, or 32 characters used as they are. FormatLaravelKey SecretFormat = "laravel_key" // FormatHex64 is 64 hexadecimal characters: 32 bytes written in hex, an // AES-256 key as Homarr's SECRET_ENCRYPTION_KEY wants one, and what // `openssl rand -hex 32` prints. FormatHex64 SecretFormat = "hex_64" )
func (SecretFormat) Check ¶ added in v0.5.0
func (f SecretFormat) Check(key, value string) error
Check refuses a value the app named by key would refuse, saying what a valid one looks like (R-105). A value is checked when somebody sets it, so the refusal arrives while they are still looking at it rather than as a crash loop after the deploy.
func (SecretFormat) Known ¶ added in v0.5.0
func (f SecretFormat) Known() bool
Known reports whether this is a format Pando can generate and check.
type Slot ¶
type Slot struct {
Key string `json:"key"`
Type SlotType `json:"type"`
Required bool `json:"required"`
Evidence []string `json:"evidence,omitempty"`
// Format is the shape the value must take, for a value the app checks:
// a Laravel APP_KEY, a Rails SECRET_KEY_BASE. What "generate a value"
// produces and what a literal is checked against (SecretFormat). Empty
// for a slot whose value Pando knows nothing about.
Format SecretFormat `json:"format,omitempty"`
// Resolution nil means unfilled. A required unfilled slot blocks deploy
// (R-132) at plan time, not at deploy time.
Resolution *Resolution `json:"resolution,omitempty"`
// Aliases are names a provisioned service also answers to on the app's
// network: the compose service it replaced. The URL is rewired to the new
// instance, but an app may reach the old name another way — a
// `while ! </dev/tcp/db/5432` wait in its start command never finished
// once `db` resolved to nothing (issue #90).
Aliases []string `json:"aliases,omitempty"`
// Scheme is how the app spells a provisioned service's URL: postgres or
// postgresql for a Postgres slot, empty for the default. Drivers disagree
// and each refuses the other's spelling — readflow's "unsupported database:
// postgresql", SQLAlchemy 2's "Can't load plugin: sqlalchemy.dialects:
// postgres" (issue #90) — so the spelling the app's own configuration used
// is the one it is given, and recorded here because nothing is read from
// the repository at deploy time (R-020).
Scheme string `json:"scheme,omitempty"`
}
Slot is a declared, typed dependency awaiting resolution.
type SlotPart ¶ added in v0.5.0
type SlotPart string
SlotPart is one piece of a slot's connection string, for a variable that holds only that piece (EnvEntry.SlotPart).
The connection string stays the slot's one value — what is provisioned, bound or pasted — and each part is read out of it at deploy. Nothing about a part is stored, so a password rotated in the URL is rotated in PGPASSWORD too.
const ( // SlotPartHost is the host name alone: "svc-01h…". SlotPartHost SlotPart = "host" // SlotPartPort is the port, or the type's default when the URL has none. SlotPartPort SlotPart = "port" // SlotPartHostPort is "host:port", what Joomla's JOOMLA_DB_HOST and // most "-e X_HOST=hostname:port" instructions take. SlotPartHostPort SlotPart = "hostport" SlotPartUser SlotPart = "user" SlotPartPassword SlotPart = "password" // SlotPartDatabase is the path without its slash: the database's name. SlotPartDatabase SlotPart = "database" )
type SlotType ¶
type SlotType string
SlotType is the kind of dependency a slot declares.
const ( SlotPostgres SlotType = "postgres" SlotMySQL SlotType = "mysql" SlotRedis SlotType = "redis" SlotS3 SlotType = "s3" SlotSMTP SlotType = "smtp" SlotMongoDB SlotType = "mongodb" // SlotMemcached is a memcached cache, reached at "host:port". It has no // URL scheme and no credentials, which is how its clients take it. SlotMemcached SlotType = "memcached" SlotUnknown SlotType = "unknown" )
func (SlotType) DisplayName ¶
DisplayName is how a slot type reads in a sentence written for someone who may not know what a port is (R-005, R-105). "This app needs a redis" is the kind of phrasing that tells a reader they are not the audience.
type Source ¶
type Source struct {
Type SourceType `json:"type"`
URL string `json:"url,omitempty"`
// Ref is what the user asked for; Commit is what runs (R-120). Auto-deploy
// advances Commit and creates a revision. A deploy never resolves Ref
// implicitly at runtime.
Ref string `json:"ref,omitempty"`
Commit string `json:"commit,omitempty"`
Subdir string `json:"subdir,omitempty"`
Image string `json:"image,omitempty"`
Digest string `json:"digest,omitempty"`
// UploadID names a stored upload — the app's own ID, for `pando deploy ./`
// (design 04 §4). An upload has no commit and no ref: there is no revision
// to resolve, and pretending otherwise would put a fabricated commit in the
// spec, which R-120 exists to prevent.
UploadID string `json:"upload_id,omitempty"`
// CredentialRef names an app-owned secret (O-3). The credential belongs to
// the app, not the person who supplied it, so an app keeps deploying after
// its author leaves; the supplier is recorded in the audit event instead.
CredentialRef string `json:"credential_ref,omitempty"`
}
Source identifies the app's input.
type SourceType ¶
type SourceType string
SourceType is where an app comes from.
const ( SourceGit SourceType = "git" SourceImage SourceType = "image" SourceUpload SourceType = "upload" )
type StandbyMode ¶ added in v0.17.0
type StandbyMode string
StandbyMode is whether an app keeps a warm standby (R-419).
const ( // StandbyDefault, or empty, is what host policy says ([P] on). StandbyDefault StandbyMode = "default" StandbyOn StandbyMode = "on" StandbyOff StandbyMode = "off" )
type Volume ¶
type Volume struct {
ID string `json:"id"`
Name string `json:"name"`
Declared VolumeSource `json:"declared"`
SizeHintBytes int64 `json:"size_hint_bytes,omitempty"`
}
Volume is persistent storage.
Top-level and referenced by mounts rather than nested in a workload, because a volume outlives any single workload definition and must survive a revision that renames the workload.
type VolumeSource ¶
type VolumeSource string
VolumeSource records how a volume came to be declared.
const ( VolumeFromCompose VolumeSource = "compose" VolumeFromUser VolumeSource = "user" VolumeFromWarning VolumeSource = "detected-warning" // VolumeFromScreening was added by an AI screener (design 10 §3). VolumeFromScreening VolumeSource = "screened" // VolumeFromImage is a path the app's image declares with VOLUME (R-200). VolumeFromImage VolumeSource = "image" )
type Warning ¶
type Warning struct {
Code string `json:"code"`
Message string `json:"message"`
Dismissed bool `json:"dismissed"`
DismissedBy string `json:"dismissed_by,omitempty"`
}
Warning is advisory. It lives in the spec and survives revisions until dismissed. A warning is never a blocker.
type Workload ¶
type Workload struct {
Name string `json:"name"`
Image string `json:"image,omitempty"`
Command []string `json:"command,omitempty"`
Entrypoint []string `json:"entrypoint,omitempty"`
WorkingDir string `json:"working_dir,omitempty"`
Env []EnvEntry `json:"env,omitempty"`
Ports []Port `json:"ports,omitempty"`
Mounts []Mount `json:"mounts,omitempty"`
Files []File `json:"files,omitempty"`
DependsOn []string `json:"depends_on,omitempty"`
Health *Healthcheck `json:"healthcheck,omitempty"`
// Aliases are other names the workload answers to on the app's private
// network, beside its own. A compose file's `container_name` is one: the
// other services address it by that name — an nginx upstream, a database
// host in a URL — and Pando, which names containers itself, made the name
// resolve nowhere (issue #90). Reachable only inside the app, like the
// workload's own name (R-026).
Aliases []string `json:"aliases,omitempty"`
// Exposed means reachable through the proxy at a sub-route (R-026).
Exposed bool `json:"exposed"`
// Primary is the workload the app's URL resolves to. Exactly one workload
// has it: the app has one canonical endpoint.
Primary bool `json:"primary"`
Resources *ResourceLimits `json:"resources,omitempty"`
// Build is how this workload's image is produced, when it is built rather
// than pulled. Nil means Image names something that already exists.
//
// Per workload because that is how a compose file describes an app: each
// service either names an image or says how to build one, and a file with
// two buildable services needs two images. The app-level Build block cannot
// say that — it is one build — which is why importing a compose file used
// to discard the build instructions entirely.
Build *WorkloadBuild `json:"build,omitempty"`
}
Workload is one process within the app's bundle.
func (Workload) HTTPPort ¶ added in v0.5.0
HTTPPort is the port a workload's web traffic goes to: the first port that carries HTTP, or the workload's only port.
Detection orders ports so the first HTTP one is the one people open (R-097), and marks the ones it knows carry something else as tcp. Of several ports none of which is HTTP, none is the answer: sending a browser to the first one listed — epmd, MQTT, a database — was how apps came to answer every request with a protocol error (issue #90).
type WorkloadBuild ¶
type WorkloadBuild struct {
Context string `json:"context,omitempty"`
Dockerfile string `json:"dockerfile,omitempty"`
Target string `json:"target,omitempty"`
// Args are this service's own build arguments, from its compose `build:
// args:`. Stored in the clear, like the app's.
Args []KV `json:"args,omitempty"`
}
WorkloadBuild is how one workload's image is built.
Deliberately smaller than the app-level Build: isolation, timeout and egress are properties of the build *environment*, which is the installation's business and the same for every workload in an app. What differs per service is only where its source is and which file describes it.