Documentation
¶
Index ¶
- Constants
- Variables
- func ApplyDefaults(m Meta, params map[string]any) map[string]any
- func IsBackupMarkerNone(marker string) bool
- func NormalizeParams(m *Meta) error
- func RenderAndValidate(body string, params map[string]any) (string, error)
- func RenderBody(body string, params map[string]any) (string, error)
- func ValidName(s string) bool
- func Validate(m Meta, params map[string]any, secrets map[string]string) error
- func ValidateAllowMissingSecrets(m Meta, params map[string]any, secrets map[string]string) error
- func ValidateIngress(ing *Ingress) error
- func ValidateParamDefs(m Meta) error
- func ValidateVolumes(m Meta) error
- func ValidateVolumesUpdate(m, stored Meta) error
- type Display
- type Ingress
- type Meta
- type ParamDef
- type PreBackup
- type Secrets
- type Volume
Constants ¶
const BackupMarkerNone = extension.BackupMarkerNone
BackupMarkerNone is the one `backup:` marker literal the core interprets: a volume declaring it is never exported by a backup, on any path. Every other marker string stays opaque and belongs to the commercial marker grammar.
It is an ALIAS of extension.BackupMarkerNone, which is the canonical definition: the literal is part of the public seam (a commercial consumer links against it rather than hardcoding "none"), and a second declaration here could drift from it. instance.BackupMarkerNone aliases this in turn.
Variables ¶
var ErrInvalidParameters = errors.New("invalid parameters")
ErrInvalidParameters is the sentinel returned by Validate.
var ErrRenderInvalid = errors.New("rendered template is invalid")
ErrRenderInvalid is returned when the rendered template output is invalid.
var NameRe = regexp.MustCompile(`^[a-z0-9][a-z0-9-]{0,38}[a-z0-9]$`)
NameRe is the DNS-label constraint for template ids and instance slugs.
It is ALSO the shape check for a new host id on POST /hosts/{host}/rename (via the API layer's validName). A change made for template-id reasons therefore silently changes what host ids the rename route will accept — and a host id that no longer matches is one nobody can rename to, even though hosts/*.yaml itself imposes no such constraint. Check both callers.
Functions ¶
func ApplyDefaults ¶
ApplyDefaults returns a copy of params with any omitted parameter filled from its ParamDef.Default. Caller-supplied values always win. Parameters without a default are left absent (Validate enforces required ones).
func IsBackupMarkerNone ¶ added in v1.0.39
IsBackupMarkerNone reports whether a raw marker is the `none` veto, folding case and trimming whitespace so the comparison fails CLOSED on a near-miss registration never saw. Re-exported from extension so render's own consumers have one comparison site; see extension.IsBackupMarkerNone for why.
func NormalizeParams ¶
NormalizeParams normalizes each parameter's Type (blank → "string") and returns an error for an unknown type (allowed: string|int|bool|select).
func RenderAndValidate ¶ added in v1.0.11
RenderAndValidate renders the template body and validates the result. It calls RenderBody then checks for issues with multi-line string params whose continuation lines lack the indentation needed to stay valid in a YAML block-scalar context.
func RenderBody ¶
RenderBody substitutes params into an already-separated template body using text/template (with missingkey=error) and returns the final YAML. Callers that hold the full source (meta + body) split it with ParseMeta first; a store.Template keeps Meta and Body apart and renders the body directly.
func Validate ¶
Validate checks that params and secrets satisfy the template's contract:
- All Required parameters are present.
- No params outside the declared parameter set.
- All PerInstance secrets are present.
- No secrets outside PerInstance (PerHostReferenced are not in this map).
Returns a single error listing every problem found.
func ValidateAllowMissingSecrets ¶
ValidateAllowMissingSecrets is Validate without the "every PerInstance secret must be present" rule. It still rejects unknown secrets and enforces required parameters. The secret-rotation path uses this to re-apply a stored spec that legitimately lacks a PerInstance secret (one never set, or added to the template after deploy) without forcing the operator to re-supply it.
func ValidateIngress ¶
ValidateIngress checks an ingress declaration: container non-empty and port in 1..65535. nil is valid (no ingress).
func ValidateParamDefs ¶ added in v1.0.26
ValidateParamDefs checks the parameter declarations themselves (as opposed to Validate, which checks supplied values against them). It rejects a parameter marked `secret: true`: parameter values land in a plaintext column, so the flag cannot mean what it says. secrets.per_instance is the encrypted-at-rest path and already does this job. (#205)
func ValidateVolumes ¶ added in v1.0.39
ValidateVolumes checks each volume's exclude patterns: non-empty, relative, no ".." segment (so a pattern cannot be read as host-absolute or escape the volume root), clean (see below), not a "/**"-suffixed pattern whose zero-segment rescue swallows everything (see below), and compilable. Rejecting at registration means a typo fails visibly instead of silently matching nothing at backup time.
Patterns are matched against path.Clean'ed tar entry names (tarfilter.go), so a pattern that is not itself already clean — a leading "./", a trailing slash, an internal "//" — can never match anything: it differs from every cleaned name by construction. That failure mode is silent (a legitimate zero-match backup and a typo'd one both show `excluded.entries: 0` in a backup row nobody reads), which is exactly what this validator exists to catch, so such a pattern is rejected outright rather than accepted and left to quietly do nothing. path.Clean does not strip a leading "/" or resolve a leading "..", so those two checks above remain load-bearing on their own and are kept ahead of this one so their more specific messages win first.
A "/**"-suffixed pattern whose stripped prefix ALSO ends in a wildcard segment ("*" or "**") and contains a "**" segment somewhere in it is rejected for a related reason: newDropper's zero-segment rescue (tarfilter.go) skips a match when the pattern's own stripped prefix matches the same entry. If that prefix's last segment is itself a wildcard, the prefix matches every entry the full pattern matches too — so the rescue fires unconditionally and the pattern silently drops nothing at all, files included, with the same invisible `excluded.entries: 0` signal. "**/b/**" is unaffected (its prefix "**/b" ends in the literal "b", not a wildcard) and stays valid; "a/**/**", "**/**", "**/*/**" and "a/**/*/**" are all rejected by this rule. A bare "**" (no "/**" suffix to strip) is untouched by this check and stays valid — it legitimately matches, and so drops, every entry.
It validates a template being CREATED: a near-miss `none` marker is rejected outright. Use ValidateVolumesUpdate for an edit of a stored template, which must not reject a near-miss the stored row already carries.
func ValidateVolumesUpdate ¶ added in v1.0.39
ValidateVolumesUpdate validates m as an UPDATE of the already-stored meta. It is ValidateVolumes with exactly one relaxation: a near-miss `none` marker that the STORED row already carries on the same volume is accepted, while one the update newly introduces (or changes to a different near-miss) is rejected as on create.
Without this, a row registered before the near-miss rule existed — precisely the population that rule was written about — becomes permanently uneditable: every PUT fails, including one changing an unrelated field, and there is no in-product path to the row to fix the marker. Consumption fails CLOSED (IsBackupMarkerNone folds case, so `None` really does veto), so grandfathering the stored value costs no safety; it just declines to hold an unrelated edit hostage. The write-path warning still names the volume on every such edit.
Types ¶
type Display ¶
type Display struct {
Name string `yaml:"name,omitempty" json:"name,omitempty"`
Description string `yaml:"description,omitempty" json:"description,omitempty"`
Category string `yaml:"category,omitempty" json:"category,omitempty"`
Icon string `yaml:"icon,omitempty" json:"icon,omitempty"`
}
Display holds human-readable presentation metadata for a template.
type Ingress ¶
type Ingress struct {
Container string `yaml:"container" json:"container"`
Port int `yaml:"port" json:"port"`
}
Ingress declares which container+port in the rendered pod serves HTTP, so the ingress layer can route a domain to it. Absent on non-web templates.
type Meta ¶
type Meta struct {
ID string `yaml:"id" json:"id"`
Display Display `yaml:"display,omitempty" json:"display,omitempty"`
Parameters []ParamDef `yaml:"parameters" json:"parameters,omitempty"`
Secrets Secrets `yaml:"secrets" json:"secrets,omitempty"`
Volumes []Volume `yaml:"volumes" json:"volumes,omitempty"`
Ingress *Ingress `yaml:"ingress" json:"ingress,omitempty"`
PreBackup *PreBackup `yaml:"pre_backup,omitempty" json:"pre_backup,omitempty"`
}
Meta describes a template's parameter and secret contract. It is parsed from the leading "# template-meta:" comment block.
func ParseMeta ¶
ParseMeta extracts the template-meta block from the head of the file and returns the rest of the file as the renderable body.
The block must look like:
# template-meta: # id: postgres # parameters: ...
The parser stops at the first non-comment line. The body is everything from that point onward (with a leading "---" preserved if present).
type ParamDef ¶
type ParamDef struct {
Name string `yaml:"name" json:"name"`
Type string `yaml:"type" json:"type"`
Required bool `yaml:"required,omitempty" json:"required,omitempty"`
Label string `yaml:"label,omitempty" json:"label,omitempty"`
Description string `yaml:"description,omitempty" json:"description,omitempty"`
Default any `yaml:"default,omitempty" json:"default,omitempty"`
Placeholder string `yaml:"placeholder,omitempty" json:"placeholder,omitempty"`
Options []string `yaml:"options,omitempty" json:"options,omitempty"`
// Secret is rejected, not honored: a template declaring it fails validation
// (ValidateParamDefs). Parameter values are stored in a plaintext column, so
// the flag once promised an encryption-at-rest guarantee the storage layer
// never delivered — it only ever redacted the value from GET responses.
// Authors want secrets.per_instance, which is encrypted at rest and reaches
// the pod via secretKeyRef. The field is kept solely so the flag is *seen*
// and refused: meta is decoded with non-strict YAML, so deleting it would
// silently ignore `secret: true` instead of failing on it. (#205)
Secret bool `yaml:"secret,omitempty" json:"secret,omitempty"`
}
ParamDef describes a single template parameter, its type, and constraints.
type PreBackup ¶ added in v1.0.14
type PreBackup struct {
Container string `yaml:"container" json:"container"`
Command string `yaml:"command" json:"command"`
}
PreBackup is a command run inside a named container immediately before the backup job stops+exports the instance. A non-zero exit fails the backup, so a failed dump never ships a stale/partial snapshot.
Command is rendered with the instance's parameters (text/template) and then run as `/bin/sh -lc "<rendered command>"` in Container. The target container must therefore provide /bin/sh and support a login profile — minimal or distroless images will fail the exec (and thus the backup). Because rendering happens before the shell, parameters are interpolated directly into the shell line.
type Volume ¶
type Volume struct {
Name string `yaml:"name" json:"name"`
// Backup is the volume's backup target/marker. The core interprets exactly
// one literal, BackupMarkerNone ("none"); every other string is opaque.
//
// A near-miss — `None`, `NONE`, `"none "` — is handled on BOTH sides, and
// they are not redundant. ValidateVolumes REJECTS such a value at
// registration, so an author who wrote `None` is told rather than guessed
// at. But registration only runs on the write path: a template row
// persisted before that validator existed is never re-validated on read, so
// every consumer ALSO compares through IsBackupMarkerNone, which folds case
// and trims whitespace. For a marker whose entire job is "never capture
// this", failing open on a typo is the wrong direction — the volume would
// be exported into every blob against the operator's explicit veto.
// The stored string is never rewritten; only the comparison is tolerant.
Backup string `yaml:"backup,omitempty" json:"backup,omitempty"`
// Exclude lists glob patterns (doublestar syntax, `**` spans separators)
// matched against each tar entry's path.Clean'ed name relative to the
// volume root. Matching entries are omitted from the volume's BACKUP tar
// and from nothing else — rename/migrate/copy always export everything.
//
// A directory's own tar entry is NEVER dropped, no matter which pattern
// matches it or how — only files and links are ever removed. VolumeImport
// recreates a directory implicitly from the paths beneath it, so a tar
// missing a directory entry its children still need would fail restore's
// integrity verification (a re-export compared against the stored
// manifest), and that failure lands only after the instance has already
// been torn down for the restore. A pattern can therefore empty a
// directory but never remove it: "dir/**" drops everything under "dir"
// while "dir" itself still ships.
Exclude []string `yaml:"exclude,omitempty" json:"exclude,omitempty"`
}