definition

package
v0.5.2 Latest Latest
Warning

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

Go to latest
Published: Jul 19, 2026 License: Apache-2.0 Imports: 25 Imported by: 0

Documentation

Overview

Package definition is the mooring.yaml definition file (plan §7.7): the declarative source of truth for an app's Mooring-managed surface. It is a SECOND front-end onto the same reconciler/§5.6 validator the dashboard drives — a new front door, never a new trust path. Nothing in it reaches `docker compose` unvalidated.

Mooring OWNS the runtime: the operator declares a multi-service STACK here and Mooring GENERATES the compose (and, for build services, the Dockerfile). There is no way to supply a raw compose/Dockerfile — `compose.source` is generated-only. `services` is a map keyed by name; per-service `env` is a map of literals/secret references (compose-familiar).

Index

Constants

View Source
const APIVersion = "mooring/v1"

APIVersion is the ONLY accepted envelope version — exact-match, fail-closed.

View Source
const SourceGenerated = "generated"

SourceGenerated is the only accepted compose source.

Variables

View Source
var ErrTampered = errors.New("definition HMAC mismatch (tampered)")

ErrTampered means a stored definition's HMAC did not verify (changed outside Mooring). It is never loaded — fail-closed.

Functions

func Canonical

func Canonical(d *Definition) ([]byte, error)

Canonical re-marshals a definition to canonical YAML (stable field order from the struct) — what is written back to canonical.yaml on every successful apply, so the stored form is always Mooring's typed render, never the operator's raw bytes.

func CanonicalHost

func CanonicalHost(h *Host) ([]byte, error)

Canonical re-marshals a host definition to canonical YAML.

func ComposeBytes

func ComposeBytes(d *Definition) ([]byte, error)

ComposeBytes returns the compose document this definition would deploy: ALWAYS generated from the typed services — Mooring owns the compose. There is no raw (repo_path/inline) source.

func Kind

func Kind(raw []byte) (string, error)

Kind peeks the `kind` field for dispatch (App vs Host). It is a loose read used ONLY to choose the typed parser — the real Parse/ParseHost then re-does the full hardened, parser-differential-resistant validation, so a mis-peek can't bypass it.

func ManagedCertDir

func ManagedCertDir(service, hostname string) string

ManagedCertDir is the run-dir-relative directory a cert binding is synced into (tls.crt + tls.key) and bind-mounted from. service + hostname are schema-validated.

func ManagedConfigPath

func ManagedConfigPath(service string, i int) string

ManagedConfigPath / ManagedSecretPath are the run-dir-relative paths where Mooring materializes a service's config file / secret file (and the compose bind source). Kept here so reconcile (which emits the mount) and the deploy (which writes the content) always agree. The service name, secret name, and index are schema-validated, so the path is traversal-free.

func ManagedSecretPath

func ManagedSecretPath(service, name string) string

func PeekMetadata

func PeekMetadata(raw []byte) (slug, name string)

PeekMetadata leniently reads metadata.slug + metadata.name from a mooring file — just enough to label a multi-file repo's chooser (which file → which app). It does NOT validate the document; the slug it returns is re-checked by gitstore.Save (the real gate) before any app is created, and the full hardened Parse runs at deploy.

func Validate

func Validate(d *Definition, runDir string, env compose.Env, protectedPaths []string) error

Types

type AckSet

type AckSet map[string]bool

AckSet is the set of field names the operator has acknowledged as widening.

type AppRegistration

type AppRegistration struct {
	Slug    string    `yaml:"slug"`
	Source  AppSource `yaml:"source"`
	Enabled bool      `yaml:"enabled"`
}

AppRegistration is one entry in the registry — the ONLY place the set of apps on the box is enumerated. Registering does NOT deploy.

type AppSource

type AppSource struct {
	Repo    string `yaml:"repo,omitempty"`
	Ref     string `yaml:"ref,omitempty"`
	Path    string `yaml:"path,omitempty"`
	Managed bool   `yaml:"managed,omitempty"`
}

AppSource is a oneOf: a repo (+ref), a local path, or managed-in-place.

type Binding

type Binding struct {
	Value  string
	Secret string
	Env    string
	App    string
	Cert   string
}

Binding resolves one {{hm.KEY}} token in a config-file template. Exactly one form: a scalar literal, or a single-key mapping selecting a source —

  • { secret: NAME } the encrypted secret value (marks the file secret-bearing)
  • { env: NAME } the SAME service's env value (literal or secret-backed)
  • { app: FIELD } a safe app field (currently only `slug`)
  • { cert: HOSTNAME.field } a path to a SAME-service cert binding's tls.crt|key|ca

This is the superset of the legacy dashboard binding sources, so the dashboard's config-file editor can express the full capability with nothing lost.

func (Binding) MarshalYAML

func (b Binding) MarshalYAML() (any, error)

MarshalYAML renders the canonical form: the single source mapping for a reference, else the scalar literal — so Canonical round-trips back through UnmarshalYAML.

func (*Binding) UnmarshalYAML

func (b *Binding) UnmarshalYAML(n *yaml.Node) error

UnmarshalYAML accepts a scalar literal or a single-key { SOURCE: ARG } mapping.

type Build

type Build struct {
	Language string            `yaml:"language,omitempty"`
	Version  string            `yaml:"version,omitempty"`
	Dir      string            `yaml:"dir,omitempty"`  // repo-relative subdir to build from (default ".")
	Base     string            `yaml:"base,omitempty"` // generic only
	Install  string            `yaml:"install,omitempty"`
	BuildCmd string            `yaml:"build,omitempty"`
	Start    []string          `yaml:"start,omitempty"`
	Env      map[string]string `yaml:"env,omitempty"`
	Packages []string          `yaml:"packages,omitempty"`
	Output   string            `yaml:"output,omitempty"` // build output dir to ship (e.g. static: "dist")
	Nonroot  *bool             `yaml:"run_as_nonroot,omitempty"`
}

Build is the declarative build spec — Mooring GENERATES the Dockerfile from it.

type CertBinding

type CertBinding struct {
	Hostname  string `yaml:"hostname"`
	Subdomain string `yaml:"subdomain,omitempty"` // XOR hostname: expands to <subdomain>.<edge.base_domain>
	Mount     string `yaml:"mount"`
	CA        string `yaml:"ca,omitempty"` // "" = default issuer; else a named CA from config.yaml edge.cas
}

CertBinding syncs a managed edge cert into a service. The edge issues AND renews the leaf; Mooring copies it into the service's mount and recreates the service at deploy AND autonomously on renewal (a background watcher re-syncs + recreates the affected service when the edge renews the leaf), so a renewed cert is picked up without a manual redeploy.

type Compose

type Compose struct {
	Source   string             `yaml:"source,omitempty"`
	Services map[string]Service `yaml:"services,omitempty"`
	Path     string             `yaml:"path,omitempty"`
	Inline   string             `yaml:"inline,omitempty"`
}

Compose is GENERATED-ONLY: Mooring owns the compose. `source` defaults to and may only be "generated". The legacy `repo_path`/`inline` sources are rejected; Path and Inline are retained ONLY so a stale definition gets a clear, guiding rejection.

type ConfigFile

type ConfigFile struct {
	Repo     string             `yaml:"repo,omitempty"`
	Template string             `yaml:"template,omitempty"`
	Mount    string             `yaml:"mount"`
	Bindings map[string]Binding `yaml:"bindings,omitempty"`
}

ConfigFile is an app config file Mooring renders + bind-mounts read-only into a service. Content is a repo path (git cat-file @ pinned commit) XOR inline template. Bindings is the explicit allowlist of {{hm.KEY}} tokens the file may resolve; the app's own ${…} survive byte-identical.

type DefaultScaling

type DefaultScaling struct {
	Max int `yaml:"max,omitempty"`
	Min int `yaml:"min,omitempty"`
}

DefaultScaling is the host-default scaling envelope (a ceiling, projectable).

type Defaults

type Defaults struct {
	Scaling     *DefaultScaling `yaml:"scaling,omitempty"`
	SelfHealing *bool           `yaml:"self_healing,omitempty"` // enable/disable the supervisor
	AutoDeploy  *bool           `yaml:"auto_deploy,omitempty"`  // default git auto-deploy (widening → ack)
}

Defaults is the global base layer projected BENEATH each app's spec (resolved before §5.6). It carries ONLY the Tier-3 subset of knobs — never edge.routes or git.ref (which don't exist here by construction) and never a Tier-1 field. A default may TIGHTEN a posture but never silently WIDEN one (see posture.go).

POSTURE-SENSITIVE: every field here must be handled by PostureWidenings() in posture.go. TestDefaultsFieldsAllPostureChecked enforces this — adding a field without a widening check fails that test, so the predicate stays closed.

type Definition

type Definition struct {
	APIVersion string   `yaml:"apiVersion"`
	Kind       string   `yaml:"kind"`
	Metadata   Metadata `yaml:"metadata"`
	Spec       Spec     `yaml:"spec"`
}

Definition is the whole mooring.yaml document (kind: App).

func Parse

func Parse(raw []byte) (*Definition, error)

Parse turns mooring.yaml bytes into a validated, typed App Definition. It is the parser-differential-resistant chokepoint (plan §7.7):

  • reject YAML anchors, aliases, and merge keys (<<) — they let one parser see a different document than another, smuggling a key past validation;
  • reject duplicate keys (last-wins ambiguity);
  • reject a SECOND YAML document;
  • reject unknown keys (KnownFields — additionalProperties:false everywhere);
  • reject any Tier-1 security field (the 3-tier boundary, §7.8);
  • exact apiVersion + kind + immutable-slug shape, fail-closed.

type Edge

type Edge struct {
	Routes   []Route   `yaml:"routes,omitempty"`
	L4Routes []L4Route `yaml:"l4_routes,omitempty"`
}

Edge is the Layer-1 route input (§6).

type EnvValue

type EnvValue struct {
	Value  string
	Secret string
}

EnvValue is a per-service env var: a literal value XOR a `{secret: NAME}` reference. A scalar is a literal; a mapping must be exactly `{ secret: NAME }`.

func (EnvValue) MarshalYAML

func (e EnvValue) MarshalYAML() (any, error)

MarshalYAML renders the canonical form: a `{secret: NAME}` mapping for a reference, else the scalar literal — so Canonical round-trips back through UnmarshalYAML.

func (*EnvValue) UnmarshalYAML

func (e *EnvValue) UnmarshalYAML(n *yaml.Node) error

UnmarshalYAML accepts a scalar literal or a `{ secret: NAME }` mapping (only).

type Git

type Git struct {
	Repo       string `yaml:"repo"`
	Ref        string `yaml:"ref"`
	AutoDeploy bool   `yaml:"auto_deploy"`
}

Git is the repo-path / auto-pull config (§7.6). AutoDeploy defaults false.

type Host

type Host struct {
	APIVersion string   `yaml:"apiVersion"`
	Kind       string   `yaml:"kind"`
	Spec       HostSpec `yaml:"spec"`
}

Host is the whole host.yaml document.

func ParseHost

func ParseHost(raw []byte) (*Host, error)

ParseHost parses + validates a kind:Host document through the same hardened, parser-differential-resistant, Tier-1-rejecting pipeline as an App.

type HostSpec

type HostSpec struct {
	Apps          []AppRegistration `yaml:"apps,omitempty"`
	Defaults      *Defaults         `yaml:"defaults,omitempty"`
	Orchestration *Orchestration    `yaml:"orchestration,omitempty"`
}

HostSpec is the server-wide surface.

type L4Route

type L4Route struct {
	Listen   int    `yaml:"listen"`        // the host port the L4 LB binds
	Protocol string `yaml:"protocol"`      // tcp | udp
	Service  string `yaml:"service"`       // selector → the service whose replicas receive traffic
	Port     int    `yaml:"port"`          // the service's internal container port
	TLS      string `yaml:"tls,omitempty"` // "" | passthrough (terminate not yet supported)
	LB       string `yaml:"lb,omitempty"`  // "" (round_robin) | least_conn | hash_client_ip
}

L4Route is one managed Layer-4 (TCP/UDP) listener: a fixed public port the L4 load balancer owns, forwarded to a service's INTERNAL replica pool. Service/Port are selectors (never a literal dial target). Use it for non-HTTP stream services (DNS 53, DoT 853, MQTTS 8883). TLS is passthrough only for now (the app terminates with a cert_binding); `terminate` is reserved for a later phase.

type Metadata

type Metadata struct {
	Slug string `yaml:"slug"`
}

Metadata carries the immutable app slug.

type NofileLimit added in v0.4.2

type NofileLimit struct {
	Soft int `yaml:"soft"`
	Hard int `yaml:"hard"`
}

NofileLimit is a soft/hard open-file-descriptor limit (compose `ulimits.nofile`).

type OpsInterface

type OpsInterface struct {
	Enabled      bool   `yaml:"enabled"`
	BaseURL      string `yaml:"base_url,omitempty"`
	SecretHeader string `yaml:"secret_header,omitempty"`
	Secret       string `yaml:"secret,omitempty"` // reference to a declared secret NAME (value resolved at deploy); never the value
	Mode         string `yaml:"mode,omitempty"`   // auto | rich | basic
	BasePath     string `yaml:"base_path,omitempty"`
	Adapter      string `yaml:"adapter,omitempty"`
}

OpsInterface is the app's optional ops endpoint (§4): Mooring probes it for RICH health/queues. Everything here is operator config EXCEPT the shared-secret VALUE — that stays encrypted (set the value in the dashboard, or declare a secret and point `secret` at it; the value never lives in this file). base_url is the in-cluster endpoint (origin only, never loopback); base_path is the relative prefix.

type Orchestration

type Orchestration struct {
	DeployOrder []OrderEdge `yaml:"deploy_order,omitempty"`
	SetupOrder  []string    `yaml:"setup_order,omitempty"`
}

Orchestration sequences multi-app deploys/setup under one operator-initiated run.

func (*Orchestration) DeploySequence

func (o *Orchestration) DeploySequence() ([]string, error)

DeploySequence returns a valid deploy order (a topological sort of the partial order: an app appears only after every app it waits on). A cycle is an error.

type OrderEdge

type OrderEdge struct {
	Deploy string `yaml:"deploy"`
	After  string `yaml:"after"`
}

OrderEdge means: the deploy of Deploy waits until After is healthy.

type Plan

type Plan struct {
	NewApp  bool
	Changes []string // changed field paths (the file is never secret-bearing, so nothing to mask)
}

Plan is the diff between the live canonical (current) and a desired definition.

func DiffPlan

func DiffPlan(current, desired *Definition) (Plan, error)

DiffPlan computes the field-level changes from current to desired. current may be nil (a brand-new app).

func (Plan) Empty

func (p Plan) Empty() bool

Empty reports whether applying the plan would be a no-op (idempotent apply).

type Port

type Port struct {
	Internal  int    `yaml:"internal"`
	Publish   bool   `yaml:"publish"`
	Public    bool   `yaml:"public"`
	Protocol  string `yaml:"protocol,omitempty"`  // "" (=tcp) | "tcp" | "udp"
	Published int    `yaml:"published,omitempty"` // host port (default = internal); maps host→container so a non-root container can bind a privileged host port
}

Port is one container port. Internal is the in-container port; Publish maps it to the host (loopback by default, all interfaces only when Public).

type Route

type Route struct {
	Hostname        string `yaml:"hostname"`
	Subdomain       string `yaml:"subdomain,omitempty"` // XOR hostname: expands to <subdomain>.<edge.base_domain>
	Service         string `yaml:"service"`
	Port            int    `yaml:"port"`
	PathPrefix      string `yaml:"path_prefix"`
	HSTS            bool   `yaml:"hsts"`
	SecurityHeaders bool   `yaml:"security_headers"`
	RedirectHTTP    bool   `yaml:"redirect_http"`
	UpstreamScheme  string `yaml:"upstream_scheme,omitempty"` // "" (=http) | http | https — how the edge dials the upstream
	CA              string `yaml:"ca,omitempty"`              // "" = default issuer; else a named CA from config.yaml edge.cas
}

Route is one managed edge vhost. Upstream is a SELECTOR — "service:port".

type Scaling

type Scaling struct {
	Service            string  `yaml:"service"`
	Enabled            bool    `yaml:"enabled"`
	Min                int     `yaml:"min"`
	Max                int     `yaml:"max"`
	UpCPUPct           float64 `yaml:"up_cpu_pct"`
	DownCPUPct         float64 `yaml:"down_cpu_pct"`
	UpMemPct           float64 `yaml:"up_mem_pct"`
	DownMemPct         float64 `yaml:"down_mem_pct"`
	PerReplicaMemMiB   int     `yaml:"per_replica_mem_mib"`
	PerReplicaCPUMilli int     `yaml:"per_replica_cpu_milli"`
	BreachForSecs      int     `yaml:"breach_for_secs,omitempty"`    // sustain window before acting (default 60)
	CooldownUpSecs     int     `yaml:"cooldown_up_secs,omitempty"`   // min seconds between scale-ups (default 60)
	CooldownDownSecs   int     `yaml:"cooldown_down_secs,omitempty"` // min seconds between scale-downs (default 300; >= up)
}

Scaling is the opt-in auto-scaling policy (§8A) for one service.

type Secret

type Secret struct {
	Name     string `yaml:"name"`
	Generate string `yaml:"generate"`
}

Secret declares a name (+ optional generate hint) — NEVER a value.

type SelfHealing

type SelfHealing struct {
	SustainTicks    int  `yaml:"sustain_ticks,omitempty"`     // failing ticks before the first remediation (anti-flap)
	AttemptCap      int  `yaml:"attempt_cap,omitempty"`       // remediations per window before the circuit opens
	StabilizeTicks  int  `yaml:"stabilize_ticks,omitempty"`   // healthy ticks required to declare RECOVERED
	OOMStrikeCap    int  `yaml:"oom_strike_cap,omitempty"`    // OOM-classified failures before short-circuiting the ladder
	WindowSeconds   int  `yaml:"window_seconds,omitempty"`    // attempt-window length; attempts reset after it elapses
	BackoffBaseSecs int  `yaml:"backoff_base_secs,omitempty"` // exponential backoff base between attempts
	BackoffMaxSecs  int  `yaml:"backoff_max_secs,omitempty"`  // backoff ceiling
	RedeployEnabled bool `yaml:"redeploy_enabled,omitempty"`  // rung-3 redeploy (≥1 GB host AND opt-in here)
}

SelfHealing tunes this app's self-healing supervisor (§8.5). Mooring supervises every service with a conservative built-in default; this block overrides the ladder tunables for ONE app. Omitted fields keep the built-in default; an omitted block leaves the app on the default entirely. All durations are in seconds.

type Service

type Service struct {
	Image        string              `yaml:"image,omitempty"` // image XOR build
	Build        *Build              `yaml:"build,omitempty"`
	Ports        []Port              `yaml:"ports,omitempty"`
	Volumes      []Volume            `yaml:"volumes,omitempty"`
	Env          map[string]EnvValue `yaml:"env,omitempty"` // KEY: literal | {secret: NAME}
	SecretFiles  []string            `yaml:"secret_files,omitempty"`
	ConfigFiles  []ConfigFile        `yaml:"config_files,omitempty"`
	CertBindings []CertBinding       `yaml:"cert_bindings,omitempty"`
	Command      []string            `yaml:"command,omitempty"`
	Healthcheck  []string            `yaml:"healthcheck,omitempty"`
	Restart      string              `yaml:"restart,omitempty"`
	DependsOn    []string            `yaml:"depends_on,omitempty"`
	OpsInterface *OpsInterface       `yaml:"ops_interface,omitempty"` // per-service ops endpoint (§4); probed for RICH health/queues/metrics
	// MemLimit sets a cgroup memory cap per replica (e.g. "768m", "1g"). It also makes the
	// autoscaler's up_mem_pct/down_mem_pct per-service: docker reports it as the container's
	// mem limit, so the trigger measures RSS against THIS budget, not the host's total RAM.
	// MemReservation is the optional soft reservation. Both empty → unbounded (host RAM), as today.
	MemLimit       string `yaml:"mem_limit,omitempty"`
	MemReservation string `yaml:"mem_reservation,omitempty"`
	// StopGracePeriod widens the SIGTERM→SIGKILL window on stop (scale-down / redeploy)
	// from docker's 10s default, so the app can drain long in-flight requests, e.g. "60s",
	// "1m30s". Empty = docker default. Pairs with the app's graceful-shutdown hooks.
	StopGracePeriod string `yaml:"stop_grace_period,omitempty"`
	// Ulimits sets per-container resource limits (currently only `nofile`, the open
	// file-descriptor cap). Raise it for services that hold many concurrent sockets
	// beyond the docker daemon default of 1024 (e.g. an MQTT broker whose
	// max_connections is otherwise clamped to the fd limit). nil = daemon default.
	Ulimits *Ulimits `yaml:"ulimits,omitempty"`
}

Service is one service in the generated stack (the map key is its name). A service is `image` (pull) XOR `build` (Mooring generates the Dockerfile).

type Setup

type Setup struct {
	Script   string   `yaml:"script"`
	Trigger  string   `yaml:"trigger"`
	Produces []string `yaml:"produces,omitempty"`
}

Setup is the per-app setup script (Mode 3), declared here and synced into the setup store; the portal is a read-only view + the gated Run (no literal paste).

type Spec

type Spec struct {
	Compose      Compose       `yaml:"compose"`
	Secrets      []Secret      `yaml:"secrets,omitempty"`
	Edge         Edge          `yaml:"edge"`
	Scaling      []Scaling     `yaml:"scaling,omitempty"` // one policy per service (auto-scale several services in one app)
	SelfHealing  *SelfHealing  `yaml:"self_healing,omitempty"`
	OpsInterface *OpsInterface `yaml:"ops_interface,omitempty"`
	Git          *Git          `yaml:"git,omitempty"`
	Setup        *Setup        `yaml:"setup,omitempty"`
}

Spec is the managed surface. Each field projects onto an existing artifact.

type Store

type Store struct {
	// contains filtered or unexported fields
}

Store persists applied canonical definitions (the history; the latest per slug is the live canonical). Every read RE-PARSES + RE-VALIDATES the stored YAML through the full pipeline (re-derive, never a verbatim replay), and the per-row HMAC is defence-in-depth so a DB tamper that still parses is caught.

func NewStore

func NewStore(db *store.DB, encKey []byte) *Store

NewStore derives a domain-separated HMAC key from the encryption key.

func (*Store) Current

func (s *Store) Current(slug string) (*Definition, error)

Current returns the live canonical definition for a slug (the latest version), HMAC-verified and RE-PARSED. No version yet → (nil, nil).

func (*Store) DeleteApp

func (s *Store) DeleteApp(ctx context.Context, slug string) error

DeleteApp removes ALL canonical-definition versions for a slug (the whole history). Used by the app-delete teardown.

func (*Store) List

func (s *Store) List(slug string) ([]VersionMeta, error)

List returns a slug's version history, newest first.

func (*Store) SaveCanonical

func (s *Store) SaveCanonical(ctx context.Context, d *Definition, note string) (int64, error)

SaveCanonical re-marshals an already-validated definition to canonical YAML and records it as a new version (which becomes the live canonical). Returns its id.

func (*Store) Version

func (s *Store) Version(slug string, id int64) (*Definition, error)

Version returns a specific past version for ROLLBACK — HMAC-verified and re-derived (re-parsed + re-validated through the full pipeline, never a verbatim replay).

type Ulimits added in v0.4.2

type Ulimits struct {
	Nofile *NofileLimit `yaml:"nofile,omitempty"`
}

Ulimits is the per-service ulimit block. Only `nofile` (max open file descriptors) is supported — the knob that gates concurrent connection count.

type VersionMeta

type VersionMeta struct {
	ID        int64
	Note      string
	CreatedAt int64
}

VersionMeta is one history row (no content).

type Volume

type Volume struct {
	Name     string `yaml:"name"`
	Source   string `yaml:"source"`
	Target   string `yaml:"target"`
	ReadOnly bool   `yaml:"read_only"`
}

Volume is a named volume XOR a run_dir-confined bind.

type Widening

type Widening struct {
	Field  string
	Detail string
}

Widening is one posture-widening a default would apply to an app (needs an ack).

func PostureWidenings

func PostureWidenings(d *Defaults) []Widening

PostureWidenings returns every posture-widening the given host defaults would apply (empty = the defaults only tighten / are neutral, applicable without acknowledgement). edge.routes and git.ref cannot appear in Defaults at all (not in the struct), so they need no check here.

func UnackedWidenings

func UnackedWidenings(d *Defaults, acks AckSet) []Widening

UnackedWidenings returns the posture-widenings NOT covered by acks — a non-empty result must BLOCK the apply (the host default would silently widen an app's posture). This is how "defaults never silently widen" is enforced.

Jump to

Keyboard shortcuts

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