render

package
v1.0.39 Latest Latest
Warning

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

Go to latest
Published: Aug 13, 2026 License: MIT Imports: 12 Imported by: 0

Documentation

Index

Constants

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

View Source
var ErrInvalidParameters = errors.New("invalid parameters")

ErrInvalidParameters is the sentinel returned by Validate.

View Source
var ErrRenderInvalid = errors.New("rendered template is invalid")

ErrRenderInvalid is returned when the rendered template output is invalid.

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

func ApplyDefaults(m Meta, params map[string]any) map[string]any

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

func IsBackupMarkerNone(marker string) bool

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

func NormalizeParams(m *Meta) error

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

func RenderAndValidate(body string, params map[string]any) (string, error)

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

func RenderBody(body string, params map[string]any) (string, error)

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 ValidName

func ValidName(s string) bool

ValidName reports whether s is a valid template id / name.

func Validate

func Validate(m Meta, params map[string]any, secrets map[string]string) error

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

func ValidateAllowMissingSecrets(m Meta, params map[string]any, secrets map[string]string) error

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

func ValidateIngress(ing *Ingress) error

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

func ValidateParamDefs(m Meta) error

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

func ValidateVolumes(m Meta) error

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

func ValidateVolumesUpdate(m, stored Meta) error

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

func ParseMeta(src string) (Meta, string, error)

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 Secrets

type Secrets struct {
	PerInstance       []string `yaml:"per_instance" json:"per_instance,omitempty"`
	PerHostReferenced []string `yaml:"per_host_referenced" json:"per_host_referenced,omitempty"`
}

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

Jump to

Keyboard shortcuts

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