Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,34 @@ its root CA so browsers accept the certificates:
caddy trust
```

## Build and Run

Bates ships two binaries: `batesd` (the server, a Mix release) and
`bates` (a thin escript client for control commands). From `source/`:

```bash
mix deps.get
mix escript.build # produces source/bates (the CLI)
MIX_ENV=prod mix release batesd # produces source/_build/prod/rel/batesd/
```

Run the daemon in the foreground:

```bash
_build/prod/rel/batesd/bin/batesd
```

Pass `--config <path>` to override the default config location
(`~/.config/bates/config.toml`). Ctrl-C shuts the daemon down.

The `bates` escript talks to the running daemon via the JSON API.
With `bates` and `batesd` both on `$PATH`:

```bash
bates status
bates env myapp
```

## Configuration

The configuration file is defined in [TOML][] format.
Expand Down
8 changes: 8 additions & 0 deletions source/config/prod.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import Config

config :bates, BatesWeb.Endpoint,
http: [port: 4080],
secret_key_base:
"8d92b2f472c7deb4054a5fa64b1f1d32572f9a1495cb6e7bdbd34837fa3352d1fe70201c37ded9d7a7e7f95d67780e2877e515839f46c51fbc5a87f9f000f67e"

config :logger, level: :info
3 changes: 2 additions & 1 deletion source/config/test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ config :bates, BatesWeb.Endpoint,

config :bates,
poll_interval: 50,
readiness_timeout: 2_000
readiness_timeout: 2_000,
skip_prereq_check: true

config :logger, level: :warning
25 changes: 25 additions & 0 deletions source/lib/bates/application.ex
Original file line number Diff line number Diff line change
@@ -1,11 +1,36 @@
defmodule Bates.Application do
use Application

alias Bates.Daemon

def start(_type, _args) do
boot_daemon()

opts = [strategy: :one_for_one, name: Bates.Supervisor]
Supervisor.start_link(children(), opts)
end

# When `:bates, :skip_prereq_check` is `true` (the default in
# `config/test.exs`) skip both argv parsing and the prereq check.
# `mix test` invokes `start/2` with `System.argv() == ["test"]`, which
# the daemon parser would reject as a stray positional argument.
defp boot_daemon do
if Application.get_env(:bates, :skip_prereq_check, false) do
:ok
else
with {:ok, opts} <- Daemon.parse_argv(System.argv()),
:ok <- Daemon.apply_options(opts),
:ok <- Daemon.apply_env(),
:ok <- Daemon.verify_prerequisites() do
:ok
else
{:error, message} ->
IO.write(:stderr, message)
System.halt(2)
end
end
end

defp children do
[
{Registry, keys: :unique, name: Bates.ProcessRegistry},
Expand Down
2 changes: 0 additions & 2 deletions source/lib/bates/cli.ex
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ defmodule Bates.CLI do
end

@doc false
def dispatch(["start" | rest]), do: Bates.CLI.Start.run(rest)
def dispatch(["setup"]), do: Bates.CLI.Setup.run()
def dispatch(["status"]), do: Bates.CLI.Status.run()
def dispatch(["up", name]) when is_binary(name), do: Bates.CLI.Up.run(name)
Expand All @@ -26,7 +25,6 @@ defmodule Bates.CLI do
defp usage do
IO.write(:stderr, """
Usage:
bates start [--config <path>]
bates setup
bates status
bates up <name>
Expand Down
2 changes: 1 addition & 1 deletion source/lib/bates/cli/client.ex
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ defmodule Bates.CLI.Client do

@doc false
def not_running_message,
do: "Bates is not running. Start it with: bates start"
do: "Bates is not running. Start it with: batesd"

@doc false
def ensure_apps do
Expand Down
2 changes: 1 addition & 1 deletion source/lib/bates/cli/setup.ex
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
defmodule Bates.CLI.Setup do
@moduledoc """
One-time system setup for `bates start`.
One-time system setup for `batesd`.

Two steps, both idempotent:

Expand Down
88 changes: 0 additions & 88 deletions source/lib/bates/cli/start.ex

This file was deleted.

103 changes: 103 additions & 0 deletions source/lib/bates/daemon.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
defmodule Bates.Daemon do
@moduledoc """
Boot helpers for the `batesd` Mix release.

`Bates.Application.start/2` calls these helpers to parse the daemon's
inputs and verify system prerequisites before bringing up the
supervision tree. Each helper is pure (returns a tagged tuple) so it
can be unit-tested without trapping `System.halt/1`.

Two input paths feed `:bates, :config_path`:

* `mix phx.server` (and any other dev/release entry point that
exposes argv) goes through `parse_argv/1` + `apply_options/1`.
* The `bin/batesd` Mix-release wrapper parses argv in shell and
sets the `BATES_CONFIG_PATH` environment variable, which
`apply_env/1` picks up. (The mix-release `start` subcommand
discards extra argv, so the wrapper translates the only flag we
accept into env.)

When both are set the env var wins (the wrapper sets it
deliberately).
"""

@env_config_path "BATES_CONFIG_PATH"
@usage "Usage: batesd [--config <path>]\n"

@doc """
Parses `argv` and returns the parsed options.

Recognizes `--config <path>`. On any unknown switch or unexpected
positional argument, returns `{:error, message}` where `message` is a
diagnostic followed by the usage banner.
"""
def parse_argv(argv) when is_list(argv) do
case OptionParser.parse(argv, strict: [config: :string]) do
{opts, [], []} ->
{:ok, opts}

{_opts, [extra | _], _} ->
{:error, "batesd: unexpected argument: #{extra}\n" <> @usage}

{_opts, _argv, [{switch, _} | _]} ->
{:error, "batesd: unknown option: #{switch}\n" <> @usage}
end
end

@doc """
Applies parsed daemon options to the application environment.

Today only `--config` is honored; the parsed value is expanded and
stored under `:bates, :config_path` so `Bates.Config.path/0` picks
it up.
"""
def apply_options(opts) when is_list(opts) do
if path = opts[:config] do
Application.put_env(:bates, :config_path, Path.expand(path))
end

:ok
end

@doc """
Applies daemon options sourced from the environment.

Reads `BATES_CONFIG_PATH` (set by the `bin/batesd` wrapper) and
stores it under `:bates, :config_path`, overriding anything
`apply_options/1` previously set. The argv path stays useful for
`mix phx.server`; the env path covers the Mix release.
"""
def apply_env(env \\ System.get_env()) when is_map(env) do
case Map.get(env, @env_config_path) do
nil ->
:ok

"" ->
:ok

path ->
Application.put_env(:bates, :config_path, Path.expand(path))
:ok
end
end

@doc """
Runs `Bates.Prerequisites.verify/0` and formats any failure for stderr.

Returns `:ok` if every prereq passes. Returns `{:error, message}` with
the same diagnostic the old `bates start` emitted when a check fails.
"""
def verify_prerequisites do
case Bates.Prerequisites.verify() do
:ok ->
:ok

{:error, reason} ->
{:error,
"""
bates: prerequisite not met: #{reason}
Run `bates setup` to configure system prerequisites.
"""}
end
end
end
6 changes: 3 additions & 3 deletions source/lib/bates/prerequisites.ex
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
defmodule Bates.Prerequisites do
@moduledoc """
System prerequisite checks gating `bates start`.
System prerequisite checks gating `batesd` startup.

Today this covers the Caddy executable being on `$PATH` and the
presence of `/etc/resolver/test`. Caddy's CA-trust step is no longer
a `bates start` prereq — it lives in `bates setup`.
a `batesd` prereq — it lives in `bates setup`.
"""

@resolver_path "/etc/resolver/test"

@doc """
Verifies all `bates start` prerequisites.
Verifies all `batesd` prerequisites.

Returns `:ok` if every check passes, otherwise `{:error, reason}`
with a user-facing message describing the first failure.
Expand Down
Loading