Macaw.Host.Ctl (macaw_host v0.3.0)

Copy Markdown View Source

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.Commands and Ctl;
  • the description (Macaw.Host.Ctl.Description), from the app's mix.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. name is what is typed ("asterisk"), usage how help shows it ("asterisk [CLI COMMAND]"), help one 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): extra docker run arguments (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 for start;
  • optionally init_flags/0 and after_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): lines status prints with the host's, report the 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

app_commands(source)

@spec app_commands(String.t() | nil) :: {:ok, String.t()} | {:error, String.t()}

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.

build(description, app_commands_source)

@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}.

compare(generated, generated)

@spec compare(String.t(), String.t()) :: :ok | {:error, String.t()}

:ok when committed is exactly generated, else {:error, message} naming the first line that differs, for mix macaw.host.ctl --check.

generate(description, app_commands_source)

@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.