spec

package
v0.2.0-beta.2 Latest Latest
Warning

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

Go to latest
Published: Sep 18, 2026 License: Apache-2.0 Imports: 14 Imported by: 0

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

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

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

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

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

View Source
const DefaultReplicas = 1

DefaultReplicas is used when a service doesn't set replicas.

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

func DiscoverPath(dir string, brandedName string) (string, error)

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

func ValidateLabels(labels map[string]string) error

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 Backup

type Backup struct {
	Schedule string `yaml:"schedule"`
	Retain   int    `yaml:"retain,omitempty"`
}

Backup describes a database's backup schedule.

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

func (e *EnvVar) UnmarshalYAML(node *yaml.Node) error

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

func (svc *Service) EffectiveReplicas() int

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

func (svc *Service) EffectiveStrategy() string

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

func Parse(data []byte) (*Spec, error)

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

func (s *Spec) Validate() error

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

type VaultRef struct {
	Path string `yaml:"path"`
	Key  string `yaml:"key"`
}

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.

Jump to

Keyboard shortcuts

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