Each task's tests were run against a reference at its end state; the end states were replayed from master in order with the gate at each step (650 to 762 tests); each skeleton compiles against its tests and fails them. The reference is kept off this machine. Lessons T27 (every wait in a test has a limit) and T28 (mutate the reference before hand-over) come from this work. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
96 lines
4.5 KiB
Markdown
96 lines
4.5 KiB
Markdown
# M4a task 03: `gatewayd.toml`
|
|
|
|
**Branch:** `m4a` (run `git switch m4a`; `git status --short` must be empty, otherwise stop)
|
|
**Commit subject:** `gatewayd: config, gatewayd.toml into a checked Config`
|
|
|
|
## Goal
|
|
|
|
`gatewayd.toml` says where Mattermost is, where the token comes from, who may talk to Boxmaker and
|
|
in which channels, and a few limits. It is **our** format: an unknown key anywhere is an error, and
|
|
a value that is wrong is refused at start with a message that names it. Spec section 3.
|
|
|
|
```toml
|
|
[mattermost]
|
|
url = "https://straylight.scylla-hammerhead.ts.net" # https or http; no path
|
|
ca_file = "/etc/boxmaker/extra-ca.pem" # optional; added to the system's roots
|
|
|
|
[secrets.mattermost_token]
|
|
credential = "mattermost-token" # exactly one of: credential, env, file
|
|
|
|
[allow]
|
|
users = ["abcdefghijklmnopqrstuvwxyz"] # Mattermost user ids (26 characters)
|
|
channels = [] # channels and group messages allowed, by id; default none
|
|
|
|
[loop]
|
|
socket = "" # empty: <home>/run/loop/loop.sock
|
|
|
|
[paths]
|
|
home = "/var/lib/boxmaker" # default: BOXMAKER_HOME, then /var/lib/boxmaker
|
|
|
|
[limits]
|
|
queue = 20
|
|
typing_every_ms = 3000
|
|
ping_every_ms = 30000
|
|
dead_after_ms = 60000
|
|
```
|
|
|
|
## Files
|
|
|
|
- Copy: `crates/gatewayd/tests/config.rs`, `crates/gatewayd/tests/support/tmp.rs`, and the
|
|
skeleton `crates/gatewayd/src/config.rs`
|
|
- Modify: `crates/gatewayd/src/lib.rs` (`pub mod config;`), `docs/implementer-log.md`
|
|
|
|
## The skeleton
|
|
|
|
Every type is written, with its serde attributes: `Config`, `MattermostConfig`, `SecretSpec`,
|
|
`SecretSource`, `AllowConfig`, `LoopConfig`, `Paths`, `Limits` (with their defaults), `ServerUrl`,
|
|
`ConfigError`, and the constant `MATTERMOST_TOKEN = "mattermost_token"`. Do not change them. The
|
|
functions are `todo!()`, each with its steps above it.
|
|
|
|
## The messages, exactly
|
|
|
|
`Config::problem` returns the first problem, in this order:
|
|
|
|
| Problem | Message |
|
|
|---|---|
|
|
| `url` fails `parse_url` | `parse_url`'s message: `[mattermost] url "<url>" must be http:// or https://, a host, an optional port, and nothing else` (the url with `{:?}`) |
|
|
| `ca_file` not absolute | `[mattermost] ca_file "<path>" must be an absolute path` (`{:?}`) |
|
|
| no `[secrets.mattermost_token]` | `[secrets.mattermost_token] is missing` |
|
|
| a secret whose `source()` fails | `[secrets.<name>] <source's message>` |
|
|
| `allow.users` empty | `[allow] users is empty: a gateway that answers nobody is a mistake` |
|
|
| an id in `allow.users` or `allow.channels` that is not `valid_id` | `[allow] "<id>" is not a Mattermost id (26 characters of a-z and 0-9)` (`{:?}`) |
|
|
| a limit that is 0 (checked in the order queue, typing_every_ms, ping_every_ms, dead_after_ms) | `[limits] <name> must be at least 1` |
|
|
|
|
`SecretSpec::source`'s messages:
|
|
|
|
| Case | Message |
|
|
|---|---|
|
|
| not exactly one of the three | `needs exactly one of credential, env and file` |
|
|
| `credential` empty or with a byte that is not an ASCII letter, digit, `_`, `.` or `-` | `credential "<name>" is not a credential name (letters, digits, _ . -)` |
|
|
| `env` empty or with a byte that is not `A-Z`, `0-9` or `_` | `env "<var>" is not a variable name (A-Z, 0-9, _)` |
|
|
| `file` not absolute | `file "<path>" must be an absolute path` |
|
|
|
|
`Config::token_source` gives `source()` of `mattermost_token`, or `[secrets.mattermost_token] is
|
|
missing`. `ConfigError`'s `Display` is `<path>: <error or message>`.
|
|
|
|
## Steps
|
|
|
|
- [ ] **1. Copy.** `git switch m4a`, then
|
|
`mkdir -p crates/gatewayd/tests/support && cp docs/plans/M4a/files/crates/gatewayd/tests/config.rs crates/gatewayd/tests/ && cp docs/plans/M4a/files/crates/gatewayd/tests/support/tmp.rs crates/gatewayd/tests/support/ && cp docs/plans/M4a/files/crates/gatewayd/src/config.rs crates/gatewayd/src/`.
|
|
Add `pub mod config;` to `crates/gatewayd/src/lib.rs`.
|
|
- [ ] **2. See it fail.** `cargo test -p gatewayd --test config`. Expected: it compiles and 7 tests
|
|
fail with `not yet implemented`.
|
|
- [ ] **3. Fill the functions one at a time**: `valid_id`, `parse_url`, `SecretSpec::source`,
|
|
`ConfigError`'s `fmt`, then the `Config` methods. `cargo check -p gatewayd` after each.
|
|
- [ ] **4. See it pass.** `cargo test -p gatewayd --test config`. Expected: 7 passed.
|
|
- [ ] **5. Run the gate.** `cargo fmt --all`, then `make gate`. Expected last line: `gate: ok`.
|
|
- [ ] **6. Log and commit.** `git add crates/gatewayd docs/implementer-log.md Cargo.lock && git commit`
|
|
|
|
## Done when
|
|
|
|
- `cargo test -p gatewayd --test config` passes; `make gate` prints `gate: ok`.
|
|
|
|
## Stop and report if
|
|
|
|
- A test wants a type changed. The types are the format; report instead.
|