Documentation
¶
Overview ¶
Package tmpl renders madock's docker templates with text/template from the standard library.
It replaces an engine written by hand — three passes over the text, one for {{{include}}}, one for {{{key}}} substitution and one for <<<if>>> — whose weaknesses were not syntactic. A condition there was a substring search for "false", so a template could not compare two values and Go grew fake config keys to stand in for the comparisons it could not express (db/type_is_mysql, db/use_default_auth_plugin). There were no loops, so the indentation of a compose file was baked into a strings.Join in Go source. And an error had no address: one unbalanced tag made the engine abandon a file with every conditional unresolved and write the result out anyway.
text/template answers all of that at no cost in dependencies. What it needs from us is three things the standard library cannot know:
- Delimiters. They stay {{{ }}} — the obvious alternative, <<< >>>, is a bash here-string, and three Dockerfile templates legitimately contain one (`IFS='.' read major minor patch <<< "{{{php/version}}}"`).
- A data tree. Config keys are slash-separated strings, so php/xdebug/enabled has to become .php.xdebug.enabled, and the string "false" has to become a real bool — every non-empty string is true in a Go template, so a config value left as a string would make every {{{if}}} fire.
- Tolerance for a key the project does not have. A shared snippet asks about memcached/enabled on a platform whose config has never heard of memcached; the old engine left the placeholder standing and read it as false. Making that a render failure would break `madock start` on every such project, so instead every key a template mentions is seeded into the tree as an empty value before execution — absent means false, as it always did. Typos are caught where they belong, by a test over the whole tree of templates, not by a fatal error on a user's machine.
Index ¶
Constants ¶
const ( LeftDelim = "{{{" RightDelim = "}}}" )
The delimiters. Kept as constants because the converter, the tests and the key audit all have to agree with the renderer about what an action looks like.
Variables ¶
This section is empty.
Functions ¶
func CompareVersions ¶
CompareVersions compares two dotted version strings: 1 if a > b, -1 if a < b, 0 if equal. A missing segment counts as zero, so "8.4" and "8.4.0" are equal.
It is a copy of configs.CompareVersions rather than a call to it, and that is the price of this package importing nothing from madock. Twelve lines against an import cycle through configs, which is where the old engine lives.
func IsLegacy ¶
IsLegacy reports whether a template is written in the old syntax.
One conditional tag is proof on its own. Otherwise it takes a placeholder that is a bare config key, since that is the one shape the new syntax cannot produce.
func Keys ¶
Keys lists every configuration key a template reads, in the slash form the configuration uses — .php.xdebug.enabled comes back as php/xdebug/enabled.
It is what replaces missingkey=error. A key the project does not carry has to stay falsy at render time, because a shared snippet asks about memcached on platforms whose config has never heard of it; so a typo cannot be caught there. It is caught here instead, over the whole tree at once, and for every platform rather than only the one somebody happened to start.
Chains rooted at a variable — $host.name inside a range — are not keys and do not appear.
func Legacy ¶
Legacy rewrites one template and reports anything a reader should look at.
A template already in the new syntax is returned untouched, so running the converter over a tree twice is a no-op — which matters, because that is exactly what a rebase does: take theirs, run it again, run the golden tests.
Types ¶
type Renderer ¶
type Renderer struct {
// Values is the flat, slash-keyed configuration, exactly as
// configs.GetProjectConfig returns it, plus whatever the caller computes
// (project_name, scope, main_service, os/user/uid …).
Values map[string]string
// Data carries what a configuration file cannot hold: an ordered list. It
// is keyed the same way and applied on top of Values, so nginx/hosts
// replaces the map the nginx/hosts/<code>/name keys would have built with a
// slice a template can range over in order and index into.
//
// This is where the joined strings went. The old engine had no loops, so
// hosts were joined in Go with the YAML indentation of a compose file baked
// into the separator (strings.Join(onlyHosts, "\n ")). The indentation
// belongs in the template that has it.
Data map[string]any
// Snippet reads an include by its template name, which is the path used in
// the template: "snippets/docker-compose/php.yml". The caller owns the
// override chain — a project's own .madock/docker/ wins over the embedded
// copy — so it lives with the caller and not here.
Snippet func(name string) (string, error)
// Port returns the host port published for a service. It is a function and
// not a value because resolving one has a side effect: it allocates the
// port, writing it into the project's port registry. Templates call it as
// {{{port "livereload"}}}, which is honest about that in a way {{{port/livereload}}}
// never was.
Port func(service string) (int, error)
// OnLegacy is called when a template turns out to be written in the old
// <<<if>>> syntax, before it is converted and rendered. Only a project's own
// overrides under .madock/docker/ can be, and they keep working — but
// silently would mean nobody ever updates them.
OnLegacy func(name string, notes []string)
}
Renderer holds everything one project's templates are rendered against.
It deliberately depends on nothing inside madock: ports are allocated through a function the caller supplies, and snippets are read through another. That keeps this package a leaf — it can be imported by configs, by project and by the shared proxy generator without an import cycle, and its tests need no madock installation on disk.
func (*Renderer) Check ¶
Check parses body and resolves every include it reaches, without executing it.
It answers one question — does this template's include set close — and it exists because the commands that render also destroy. `rebuild` and `restart` stop the containers first, so an include that no longer resolves used to end the process with the environment down and a message about a file path. Resolving the includes is cheap, needs no configuration values, and can therefore happen before anything is torn down.
Execution is deliberately not attempted: it needs the project's data and can fail for reasons that are not drift, and a preflight that can fail for unrelated reasons is one people learn to ignore.
type Report ¶
Report is what a conversion did, in the shape both callers print.
func ConvertTree ¶
ConvertTree rewrites a directory of templates from the old <<<if>>> syntax.
It lives here rather than in the command that calls it because two callers need it and one of them is a user's: `madock template:convert` is the whole answer for somebody with an override under .madock/docker/, who has a binary and no Go toolchain to run tools/tmplconvert with. Documenting a `go run` for them was a hole, and this is what closes it.