Macaw.Host.Ctl (macaw_host v0.1.4)

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.

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

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

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.