CLI reference¶
doppel serve [--workers <n>] [--config <path>] [--store file|postgres]
[--database-url <dsn>] [--config-name <name>]
doppel config validate [same store flags]
doppel config reload [--socket <path>] [same store flags]
doppel config push [--config <path>] --database-url <dsn>
[--config-name <name>] [--if-revision <hex>]
doppel config pull --database-url <dsn> [--config-name <name>]
[--output <path>]
doppel config migrate --database-url <dsn>
doppel version
Exit codes¶
Scripts depend on these, so they are a contract:
| Code | Meaning |
|---|---|
0 |
Success |
1 |
The configuration was rejected, or the command could not do its work |
2 |
A usage error, from argument parsing |
There is no longer a code for "this build cannot do that". It went with the refusal it described when the PostgreSQL store landed: an exit code nothing can produce is a promise a script waits on forever.
Store flags¶
Where the configuration lives. These come from the command line and the environment only, never from the configuration document -- reaching the store cannot depend on reading the store.
| Flag | Environment | Default |
|---|---|---|
--store |
DOPPEL_CONFIG_STORE |
file |
--config |
DOPPEL_CONFIG_PATH |
./main.yaml |
--config-name |
DOPPEL_CONFIG_NAME |
default |
--database-url |
DOPPEL_DATABASE_URL |
none |
Precedence is command line, then environment, then default.
--database-url is required with --store postgres, and there is no local
default: a mistyped environment would otherwise connect to whatever database
happened to be at hand. See Configuration storage for what each
store does.
A database URL is masked wherever it can surface -- in an error message, in a log line, in a debug formatting of the arguments. An unparseable value is replaced wholesale rather than echoed, since it may still hold a secret.
serve¶
Builds the runtime, then loads and validates the configuration, then binds the proxy port, the admin port and the control socket.
--workers / DOPPEL_WORKERS sets the tokio runtime's worker threads;
absent, the runtime sizes itself to the machine. It is an argument rather than
a configuration field because the runtime has to exist before a
database-backed store can be opened, and the store is where the configuration
is. --workers 0 is a usage error: the value is parsed as a non-zero integer,
so zero cannot reach the runtime builder, which would panic on it.
Shutdown on SIGINT or SIGTERM stops accepting, drains in-flight requests
for up to 30 seconds, removes the control socket and exits 0. A second signal
exits at once.
config push¶
Reads a YAML document and writes it into the database. Both ends are named
separately rather than through --store, because this command always reads a
file and always writes a database: a store selector would be a flag with one
legal value and a misleading name.
Unconditional by default, which is what provisioning wants. --if-revision
<hex> makes it the same compare-and-swap the admin API uses, so a scripted
push cannot overwrite a change made in between; a revision that has moved is
refused and nothing is written. A malformed --if-revision is refused before
the database is touched, and is not reported as a mismatch -- a typo is not a
stale copy, and the two need different fixes.
An invalid document is refused with every violation listed, and writes nothing.
config pull¶
Reads the database and writes canonical YAML to --output, or to stdout when
there is none, so doppel config pull > main.yaml produces a file push
accepts unchanged. Comments and layout are not preserved; they are no part of
what the database stores.
Pulling a name that is not there fails without creating the output file.
config migrate¶
Applies any migrations the database has not seen, and reports how many ran. Safe to run twice. Never run at startup -- see Configuration storage.
config migrate --status¶
Reports the schema state and changes nothing.
schema version 1; 1 migration applied
up to date
Exit 0 when every embedded migration is applied, complete and unchanged;
1 otherwise, with a line per problem:
schema version 1; 1 migration applied
changed: migration 1 (initial) does not match the file this binary carries
run `doppel config migrate`
A migration applied to the database that this binary does not carry is
reported as unknown and does not make the command exit 1 -- everything
this binary requires is still applied, and the usual cause is an older binary
looking at a database a newer one has migrated.
config validate¶
Reports every violation, not just the first, each with its path in the
configuration. Prints them on stdout and exits 1 if there were any.
It performs no filesystem work beyond reading the configuration -- deliberately,
so it answers the same on a developer's machine as in production. Checks that
depend on the machine, such as whether the templates directory can be created,
belong to serve.
config schema¶
Prints the configuration's JSON Schema on stdout.
doppel config schema > doppel-config.schema.json
Takes no flags and reads nothing -- not even --config. The schema describes
what a configuration may contain, which is a property of the binary rather
than of any file, so accepting a path would only raise the question of whether
the answer depended on it.
The same document is checked into the repository at
doppel-config.schema.json
and attached to every release. See
Editor support for pointing an editor at it.
It is generated from the same types the admin API's OpenAPI document is built
from, so it cannot describe a field that does not exist. scripts/config_schema.py
regenerates the checked-in copy and --check fails when it is stale; CI runs
the check.
config reload¶
Connects to the control socket and asks a running server to reload.
doppel config reload --socket /tmp/doppel.sock
Without --socket it reads the socket path from the configuration, which means
it consults the store; with --socket it does not, so the store flags are
irrelevant in that case.
A reload is all or nothing: the new configuration is loaded, validated and compiled before anything is swapped, so a failure leaves the running server untouched. The response reports the revision now in effect, the number of proxies, and any configuration section that changed but needs a restart to take effect.
token add¶
Asks a running server to issue an admin token.
doppel token add --name ci --group admin --socket /tmp/doppel.sock
token `ci` issued to group `admin`, revision 4213...
it is in force now, and this is the only time it is shown:
3f2504e0-4f89-41d3-9a0c-0305e82c3301
The value is generated by the server, appended to the stored configuration,
and brought into force before the command answers -- so the token works on the
next request rather than after someone remembers to reload. It is printed
alone on the last line, so doppel token add --name ci | tail -1 captures it
without a parser.
It is shown once. Nothing stores it in a form this command could read back: the copy that exists is in the configuration, and that is the copy to guard.
--group defaults to user. That is deliberate and worth knowing before
scripting it: user carries no write access under the default access block,
so a token meant for administration needs --group admin spelled out. A
default that granted administration would make one forgotten flag an
escalation.
Socket resolution matches config reload: --socket wins and consults no
store, otherwise the path comes from the configuration.
Exit 1 if the name is already taken, if the store refuses the write, or if
the socket cannot be reached. Exit 2 if --name or --group is not a legal
name -- that is refused during argument parsing, before anything is sent.
version¶
Prints the version and exits 0.