Documentation
¶
Overview ¶
Package spec parses and validates the app.yaml file: the one declarative file a user writes in their repo, per the app spec design. Validation happens in two layers. Structural shape (required fields, enums, string patterns, unknown-key rejection) is checked against the embedded JSON Schema (schema/app.schema.json), kept as the single source of truth so CI and the deploy-time check share one definition, not two hand-maintained copies that drift. Rules the schema can't express (a field's validity depending on another field's value) are checked by Validate after parsing.
Index ¶
Constants ¶
const ( MaxLabels = 32 MaxLabelKeyLength = 255 MaxLabelValueLength = 4096 )
Sanity limits on custom labels: a genuine safety bound against unbounded container metadata growth, not a business threshold, so a fixed constant is appropriate here (the project's "no hardcoded thresholds" rule targets values a deployment might reasonably need to tune, like a retention window or a rate limit; a handful of operator hand-written monitoring/tooling labels per service isn't that). Docker itself doesn't publish an official label count/size limit, so these are chosen generously enough for real use (a monitoring agent or log shipper typically wants a handful of keys) while still bounding how much metadata one service can accumulate.
const ( BuildDockerfile = "dockerfile" BuildCompose = "compose" BuildRailpack = "railpack" BuildStatic = "static" BuildImage = "image" )
Build input types: the app spec's build.type values, matching the BuildKit-based build detection (Dockerfile, Compose, Railpack, static) plus one non-build type: a prebuilt image already sitting in a registry, deployed as-is with no build step at all (see Build.Image and internal/deploy's deployImage).
const ( StrategyRolling = "rolling" StrategyRecreate = "recreate" StrategyBlueGreen = "blue-green" )
Deploy strategies, part of the app spec. Blue-green is the effective default, since it's easier to get right than rolling with a single replica, applied by DefaultStrategy when Strategy is empty, not baked into the schema itself.
const ( EngineFake = "" // zero value only, never valid; see Validate EnginePostgres = "postgres" EngineRedis = "redis" EngineMySQL = "mysql" EngineMongoDB = "mongodb" EngineMariaDB = "mariadb" EngineKeyDB = "keydb" EngineClickHouse = "clickhouse" EngineDragonfly = "dragonfly" )
Supported managed database engines: Postgres and Redis shipped as first-class resources in the initial release, MySQL, MongoDB, MariaDB, KeyDB, ClickHouse, and Dragonfly joined once internal/reconcile/database's controller grew matching engine cases.
const DefaultReplicas = 1
DefaultReplicas is used when a service doesn't set replicas.
const ReservedLabelPrefix = "platform-reserved."
ReservedLabelPrefix is the Docker label key namespace no operator- supplied custom label may use, keeping it open for this platform's own bookkeeping labels (none exist yet: today a service's containers are found purely by name pattern, internal/reconcile/application's ContainerName/ownsContainer, not by label). It's a fixed, brand- independent string on purpose, not derived from brand.ShortName: brand.ShortName is runtime-configurable (APP_BRAND_SHORT_NAME), and a validation boundary keyed to a value that can change between deploys would make a previously-valid app.yaml unpredictably valid or invalid depending on unrelated brand config. It also has to hold for cmd/levelrail-cli, which validates a local app.yaml with no server connection and therefore no brand config to consult at all. This package stays decoupled from internal/brand for the same reason DiscoverPath (discover.go) does.
Variables ¶
This section is empty.
Functions ¶
func DiscoverPath ¶
DiscoverPath finds the app spec file in dir, checking candidate filenames in order and returning the first one that exists. The spec file is discovered by a list of candidate filenames (app.yaml, deploy.yaml, plus the branded one) so a product rename does not break existing users later. The generic names are checked first for exactly that reason: if the product is ever renamed, brandedName changes, but a repo with an app.yaml already committed keeps working without the user touching anything.
brandedName is the caller-supplied branded filename stem (derived from brand.Brand, not imported here, so this package stays decoupled from internal/brand); pass "" to skip it, e.g. in tests.
func ValidateLabels ¶
ValidateLabels checks labels against the rules above: reserved prefix, empty keys, and the sanity limits. Exported so internal/api's validateAppResource can run the identical check on API-sourced labels (which bypass Parse/Validate entirely, they're never described in app.yaml), the same "define once here, both layers call it" shape StrategyRolling/StrategyRecreate/StrategyBlueGreen already establish for the API layer to reference rather than re-derive.
Types ¶
type Build ¶
type Build struct {
Type string `yaml:"type"`
Path string `yaml:"path,omitempty"`
// BaseDirectory scopes the build context to a subdirectory of the
// repo, e.g. "apps/web" in a monorepo. Empty means the repo root.
// Meaningful for dockerfile, railpack, and static; not meaningful
// for image (nothing gets built) or compose (the compose file's own
// context: field already scopes each service).
BaseDirectory string `yaml:"baseDirectory,omitempty"`
// Image is a full registry reference (e.g.
// "ghcr.io/org/app:v1.2.3"), only meaningful for build.type: image:
// a CI pipeline (or anything else) already built and pushed this
// exact image, so there is nothing for this control plane to build,
// only to deploy as-is. See internal/deploy's deployImage.
Image string `yaml:"image,omitempty"`
// RegistryCredential names a store.RegistryCredential (by its Name,
// not ID: app.yaml is hand-written, an opaque ID would be hostile
// to author) to authenticate with when pulling Image from a private
// registry. Empty means an unauthenticated (public) pull.
RegistryCredential string `yaml:"registryCredential,omitempty"`
// Args are Dockerfile build-time ARG values (e.g. a base image
// version, a build-time feature flag), passed through as
// --build-arg equivalents to BuildKit. Only meaningful for
// build.type: dockerfile.
Args map[string]string `yaml:"args,omitempty"`
}
Build describes how a service's image gets built.
type Database ¶
type Database struct {
Engine string `yaml:"engine"`
Version string `yaml:"version,omitempty"`
Backup *Backup `yaml:"backup,omitempty"`
// EphemeralInPreviews opts this database into a disposable,
// preview-scoped instance of its own (a full container, its own
// volume, its own credentials) rather than sharing whatever this
// database resolves to in production: one per pull request, created
// alongside the preview and destroyed with it, with no restore path
// once torn down. Off by default, like every other opt-in toggle in
// this codebase; only meaningful for a database attached to an app
// with preview environments enabled (store.GitSource.PreviewEnabled).
EphemeralInPreviews bool `yaml:"ephemeralInPreviews,omitempty"`
}
Database is one entry under databases:.
type EnvVar ¶
type EnvVar struct {
// Value is a literal value, set when the YAML node was a plain
// string scalar rather than a mapping.
Value string
// From references another resource's computed value, e.g.
// "postgres.main.url". Mutually exclusive with Value, Secret, and
// Vault.
From string
// Secret means the operator provides this at deploy time via
// the platform's own envelope-encrypted secret storage, never
// written to app.yaml or the git repo. Mutually exclusive with
// Vault: a given env var resolves its value from exactly one of
// the two secret sources, never both.
Secret bool
// Required, only meaningful alongside Secret: fail the deploy if no
// value has been provided, rather than starting the container with
// the variable unset.
Required bool
// Vault means this value is resolved live from an external
// HashiCorp Vault instance at container-create time
// (internal/reconcile/application's resolveEnv), never stored by
// this platform at all: the alternative to Secret above, not a
// variant of it. nil means not vault-backed.
Vault *VaultRef
}
EnvVar is one entry in a service's env block. app.yaml allows two shapes for the same field, per the app spec's own example, which shows both:
DATABASE_URL: { from: postgres.main.url }
API_KEY: { secret: true, required: true }
A plain string scalar is also accepted as shorthand for a literal value, since forcing every simple env var into object form would make the common case more verbose than the shape it's modeled on (Docker Compose's own env shorthand).
func (*EnvVar) UnmarshalYAML ¶
UnmarshalYAML implements the string-or-object union described on EnvVar, using yaml.v3's node-based Unmarshaler interface: the node's Kind tells a scalar apart from a mapping before deciding which shape to decode into, rather than trying one and falling back on error.
type Health ¶
type Health struct {
Readiness *Probe `yaml:"readiness,omitempty"`
Liveness *Probe `yaml:"liveness,omitempty"`
}
Health holds a service's readiness and liveness probe configuration.
type Hooks ¶
type Hooks struct {
PreDeploy string `yaml:"preDeploy,omitempty"`
PostDeploy string `yaml:"postDeploy,omitempty"`
}
Hooks are the two deploy-lifecycle commands a service can declare. Both run via "sh -c" inside the newly created container (see internal/reconcile/application.Controller's own doc comment for the full timing and failure-handling contract): PreDeploy before the container's readiness probe and before any old container is retired, PostDeploy after the whole replica set has cut over. Either field may be set alone.
type Probe ¶
type Probe struct {
Path string `yaml:"path"`
Interval string `yaml:"interval,omitempty"`
Timeout string `yaml:"timeout,omitempty"`
Failures int `yaml:"failures,omitempty"`
}
Probe is a single HTTP health check.
type Resources ¶
type Resources struct {
Memory string `yaml:"memory,omitempty"`
CPU float64 `yaml:"cpu,omitempty"`
// SwapMemory caps memory plus swap combined, the same "512Mi"/"1Gi"
// shape as Memory (Docker's own MemorySwap semantics); only
// meaningful alongside Memory, see Validate.
SwapMemory string `yaml:"swapMemory,omitempty"`
// CPUSet pins the container to specific host CPUs, Docker's own
// cpuset-cpus format (e.g. "0-3" or "0,2").
CPUSet string `yaml:"cpuSet,omitempty"`
}
Resources holds a service's resource limits.
type Service ¶
type Service struct {
Build Build `yaml:"build"`
Domains []string `yaml:"domains,omitempty"`
Port int `yaml:"port,omitempty"`
// HostPort pins the host-side port Docker binds Port to, 0 meaning
// auto-assign (the same zero-value-means-unset convention Port
// itself uses). Its storage home is store.DesiredService.HostPort
// (migrations/0056_service_host_port.sql).
HostPort int `yaml:"host_port,omitempty"`
Health *Health `yaml:"health,omitempty"`
Resources *Resources `yaml:"resources,omitempty"`
Env map[string]EnvVar `yaml:"env,omitempty"`
Replicas int `yaml:"replicas,omitempty"`
Strategy string `yaml:"strategy,omitempty"`
// Labels are arbitrary operator-supplied Docker labels applied to the
// service's container at create time, an escape hatch for tooling
// this platform doesn't know about (a monitoring agent or log
// shipper that keys off container labels, a homegrown script, and
// so on). See ValidateLabels (labels.go) for what's rejected:
// notably, any key under ReservedLabelPrefix, kept open for this
// platform's own bookkeeping labels.
Labels map[string]string `yaml:"labels,omitempty"`
// Volumes are named Docker volumes or host-directory bind mounts this
// service's container mounts (see Volume's own doc comment for how
// the two are distinguished): named volumes were previously a
// database-only capability, bind mounts a compose-import-only one. A
// named volume's Name is a logical name scoped to this service, not a
// global Docker volume name (see internal/deploy's translation into
// store.ServiceVolume for the actual, platform-prefixed name); two
// services can each declare a volume named "data" without colliding.
Volumes []Volume `yaml:"volumes,omitempty"`
// Hooks are shell commands the reconciler runs inside this service's
// own container at defined points in a deploy (internal/reconcile/
// application's controller). Nil means neither is configured, the
// same "declarative, resolved before storing" shape Health/Resources
// already follow.
Hooks *Hooks `yaml:"hooks,omitempty"`
// Command overrides the image's own default CMD
// (store.DesiredService.Command), nil meaning the image's own
// default: the app.yaml equivalent of
// internal/compose.Service.Command, which this mirrors. A plain argv
// list, never shell-interpreted.
Command []string `yaml:"command,omitempty"`
}
Service is one entry under services:.
func (*Service) EffectiveReplicas ¶
EffectiveReplicas returns svc.Replicas, or DefaultReplicas if unset. Schema validation guarantees Replicas is never negative when set; zero means "not specified in app.yaml", not "zero replicas".
func (*Service) EffectiveStrategy ¶
EffectiveStrategy returns svc.Strategy, or the default (blue-green, since it's easier to get right than rolling with a single replica) if unset.
type Spec ¶
type Spec struct {
Version int `yaml:"version"`
Services map[string]Service `yaml:"services"`
Databases map[string]Database `yaml:"databases,omitempty"`
}
Spec is a fully parsed, schema-valid app.yaml.
func Parse ¶
Parse validates raw app.yaml bytes against the embedded JSON Schema, then unmarshals into a Spec and runs the semantic checks the schema can't express. Both layers must pass for Parse to succeed; a caller never sees a Spec that's schema-valid but semantically broken, or vice versa.
func (*Spec) Validate ¶
Validate checks rules the JSON Schema can't express: values whose validity depends on another field, or on the rest of the document, not just their own shape. Parse always runs this after schema validation succeeds, callers building a Spec by hand (tests, future tooling) should call it too before trusting a Spec.
type VaultRef ¶
VaultRef is app.yaml's { vault: { path, key } } env var shape: Path is the Vault KV secret's path, Key is the field name within that secret's data. Both required together, mirroring internal/store's own VaultEnvRef which this parses into (see internal/deploy's vaultEnvRefs).
type Volume ¶
type Volume struct {
Name string `yaml:"name,omitempty"`
HostPath string `yaml:"hostPath,omitempty"`
Path string `yaml:"path"`
ReadOnly bool `yaml:"readOnly,omitempty"`
}
Volume is one entry under a service's volumes:. Exactly one of Name (a named Docker volume) or HostPath (a bind mount of a real host directory, gated the same as internal/compose's own bind-mount support: see validateBindMountHostPath) is set; see Validate. ReadOnly is only meaningful alongside HostPath, matching store.ServiceBindMount.