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.
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 defines commands/0
and run/3, so it can neither replace a generic module nor carry a
second @data_dir. 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 defines commands/0
and run/3, so it can neither replace a generic module nor carry a
second @data_dir. 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.