Help us shape the future of Go! Take the 2026 Go Developer Survey

spec

package
v0.9.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 18, 2026 License: AGPL-3.0 Imports: 16 Imported by: 0

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

View Source
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).

View Source
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.

View Source
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

View Source
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.

View Source
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

func Address(host, slug string, r Routing) string

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 BuildInputs added in v0.5.0

func BuildInputs(s *AppSpec) string

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

func CheckPathPrefix(p string) *errs.Error

CheckPathPrefix refuses a path an app cannot be served under, saying why.

func CheckSlotValue added in v0.5.0

func CheckSlotValue(s *AppSpec, slot, value string) error

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

func CheckSourceURL(src Source) error

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 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

func EnvSlot(s *AppSpec, workload, key string) (string, bool)

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

func FillSlotLiteral(s *AppSpec, slot, secretRef string) bool

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

func FillSlotValue(s *AppSpec, slot, value string) bool

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 MarkSlotOptional

func MarkSlotOptional(s *AppSpec, slot string) bool

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 NormalizePathPrefix

func NormalizePathPrefix(p string) string

NormalizePathPrefix is a path as Pando stores it: one leading slash, no trailing one, lowercase.

func SetEnv

func SetEnv(s *AppSpec, workload, key string, entry EnvEntry)

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

func SetHostname(s *AppSpec, hostname string) error

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

func SlotPartOf(value string, part SlotPart) (string, bool)

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

func SlotSecretKey(slot string) string

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 Validate

func Validate(s *AppSpec) error

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"`

	// 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

func Carry(pinned, next *AppSpec) *AppSpec

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) PrimaryWorkload

func (s *AppSpec) PrimaryWorkload() (Workload, bool)

PrimaryWorkload returns the workload the app's URL resolves to.

func (*AppSpec) Slot

func (s *AppSpec) Slot(key string) (Slot, bool)

Slot returns a declared slot by key.

func (*AppSpec) Volume

func (s *AppSpec) Volume(id string) (Volume, bool)

Volume returns a declared volume by ID.

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 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

func (d Defaults) Apply(s *AppSpec, slug string)

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 Compare

func Compare(old, next *AppSpec) Diff

Compare classifies every difference from old to new.

func (Diff) Class

func (d Diff) Class() Class

Class returns the worst class present, or Benign when there are no changes.

func (Diff) Empty

func (d Diff) Empty() bool

Empty reports whether the specs are equivalent.

func (Diff) RequiresConfirmation

func (d Diff) RequiresConfirmation() bool

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

func (e Egress) Same(o Egress) bool

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 KV

type KV struct {
	Key   string `json:"key"`
	Value string `json:"value"`
}

KV is a name/value pair.

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.

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"
)

func (SlotPart) Known added in v0.5.0

func (p SlotPart) Known() bool

Known reports whether this is a part Pando can read out of a connection string. Empty — the whole string — is.

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

func (t SlotType) DisplayName() string

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 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

func (w Workload) HTTPPort() (int, bool)

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL