Generates an app's host control script, <app>-ctl, so the apps of the
family stop hand-maintaining thousands of lines of the same script.
The script follows the deployable-containers pattern: a single Elixir
file, standard library only, with one line of host state (@data_dir,
in Ctl.Home, the first module), every docker argv built in Ctl.Docker,
and the host touched only through the swappable Ctl.World. It is built
from three parts:
- the generic modules, EEx templates in
priv/ctl/compiled into this module:Ctl.Home,Ctl.App,Ctl.World,Ctl.Script,Ctl.Secret,Ctl.Env,Ctl.Docker,Ctl.CommandsandCtl; - the description (
Macaw.Host.Ctl.Description), from the app'smix.exs, which fills in everything app-specific; - the app's own commands, a source file defining
Ctl.AppCommands, spliced in as it is.
generate/2 is pure, so it is tested without Mix; mix macaw.host.ctl
writes its result, and --check compares it with the committed file.
The Ctl.AppCommands contract
The app's file holds one module, Ctl.AppCommands, and nothing beside
it: a module of its own goes inside it (Ctl.AppCommands.Helpers), and
code outside it, which would run every time the script loads, is
refused. The module has:
commands/0:[{name, usage, help}], the app's commands.nameis what is typed ("asterisk"),usagehowhelpshows it ("asterisk [CLI COMMAND]"),helpone sentence. A name the script already has (start,config…) is never dispatched here.run(name, env, args): runs one, returning what the common commands do::ok,{:error, message}(printed, exit 1) or{:halt, status}. It is called only once the data directory is set.- optionally
run_args(env): extradocker runarguments (more ports, say), placed before DOCKER_RUN_ARGS; - optionally
validate(env): the app's own checks of its host settings; - optionally
before_start(env): the app's precondition forstart; - optionally
init_flags/0andafter_init(env, flags): init's own flags for the app, and the step after init, safe to rerun; - optionally
seeds(env): app config init seeds from this host's settings; - optionally
status_lines(env, report): linesstatusprints with the host's,reportthe running app's own report (nil when it isn't running or doesn't answer).
Spec 06 §4 is the contract in full.
It builds its docker command lines with Ctl.Docker (exec_args/3,
rpc_args/2, eval_args/3, release_args/4), runs them with
Ctl.Commands.stream/1 or interactive/1, checks the container with
Ctl.Commands.ensure_running/1 or ensure_ready/1, and reads what it
manages from Ctl.App. A secret goes to the container by name, through
eval_args/3 or release_args/4, never on a command line.
Summary
Functions
Checks the source of an app's commands: it parses, holds nothing but the
one Ctl.AppCommands module (no other module, no code outside it,
which would run whenever the script loads), and itself (not a nested
module) defines commands/0 and run/3, so it can neither replace a
generic module nor carry a second @data_dir; a status_lines/1, the
hook's old arity, is refused naming status_lines/2. nil is the module of no commands.
As generate/2: {:ok, script} or {:error, message}.
:ok when committed is exactly generated, else {:error, message}
naming the first line that differs, for mix macaw.host.ctl --check.
The script for description (a :ctl keyword list, or a map
Macaw.Host.Ctl.Description.new/2 returned) and the source of the app's
Ctl.AppCommands (nil for none). Raises ArgumentError naming the fault
when either is invalid.
Functions
Checks the source of an app's commands: it parses, holds nothing but the
one Ctl.AppCommands module (no other module, no code outside it,
which would run whenever the script loads), and itself (not a nested
module) defines commands/0 and run/3, so it can neither replace a
generic module nor carry a second @data_dir; a status_lines/1, the
hook's old arity, is refused naming status_lines/2. nil is the module of no commands.
@spec build(keyword() | Macaw.Host.Ctl.Description.t(), String.t() | nil) :: {:ok, String.t()} | {:error, String.t()}
As generate/2: {:ok, script} or {:error, message}.
:ok when committed is exactly generated, else {:error, message}
naming the first line that differs, for mix macaw.host.ctl --check.
@spec generate(keyword() | Macaw.Host.Ctl.Description.t(), String.t() | nil) :: String.t()
The script for description (a :ctl keyword list, or a map
Macaw.Host.Ctl.Description.new/2 returned) and the source of the app's
Ctl.AppCommands (nil for none). Raises ArgumentError naming the fault
when either is invalid.