Decompose architecture into 23 atomic tasks across 7 parallel generations
Task graph covers all Phase 1 concerns: config system, TLS termination, proxy handler, operations (rate limiting, logging, health check, admin socket, signals, shutdown, body size limit), deployment artifacts, and two review checkpoints. No circular dependencies. Critical path length of 7. Risk distribution: 3 high-risk (ACME, TLS listener setup, startup orchestration), 7 medium, 11 low, 2 trivial.
This commit is contained in:
64
tasks/config/cli-parsing.md
Normal file
64
tasks/config/cli-parsing.md
Normal file
@@ -0,0 +1,64 @@
|
||||
---
|
||||
id: config/cli-parsing
|
||||
name: Implement CLI argument parsing with clap and config file loading
|
||||
status: pending
|
||||
depends_on: [config/static-config, config/validation]
|
||||
scope: narrow
|
||||
risk: low
|
||||
impact: component
|
||||
level: implementation
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Implement the CLI entry point using `clap` with derive macros. The CLI reads the config file, deserializes both static and dynamic portions, validates, and returns the parsed config.
|
||||
|
||||
### CLI Interface
|
||||
|
||||
```
|
||||
reverse-proxy [OPTIONS]
|
||||
|
||||
Options:
|
||||
--config <PATH> Path to config file [default: /etc/reverse-proxy/config.toml]
|
||||
--validate Validate config and exit
|
||||
--allow-wildcard-bind Permit 0.0.0.0 as a bind address (for container deployments)
|
||||
--help Show help
|
||||
--version Show version
|
||||
```
|
||||
|
||||
The `--allow-wildcard-bind` flag is OR'd with the config `allow_wildcard_bind` field — if either is set, wildcard binding is allowed.
|
||||
|
||||
### Behavior
|
||||
|
||||
- `--validate`: Load and validate the config, print success or errors, exit 0 or 1
|
||||
- Normal run: Load, validate, return config for the startup sequence
|
||||
- Config file not found: exit with error
|
||||
- Config validation fails: exit with code 1 and log all errors
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `clap` with derive macros for CLI parsing
|
||||
- [ ] `--config` flag with default `/etc/reverse-proxy/config.toml`
|
||||
- [ ] `--validate` flag: loads, validates, reports, exits
|
||||
- [ ] `--allow-wildcard-bind` flag: OR'd with config value
|
||||
- [ ] `--version` prints version from `Cargo.toml`
|
||||
- [ ] Config file loading and TOML deserialization
|
||||
- [ ] Validation runs on every load (startup and `--validate`)
|
||||
- [ ] Error messages are clear and actionable
|
||||
- [ ] Unit tests for CLI argument parsing
|
||||
- [ ] Integration test: `--validate` with valid config exits 0
|
||||
- [ ] Integration test: `--validate` with invalid config exits 1 and reports errors
|
||||
|
||||
## References
|
||||
|
||||
- docs/architecture/operations.md — CLI interface
|
||||
- docs/architecture/config.md — config loading, validation
|
||||
- docs/architecture/decisions/016-explicit-bind-address.md — `allow_wildcard_bind`
|
||||
|
||||
## Notes
|
||||
|
||||
> To be filled by implementation agent
|
||||
|
||||
## Summary
|
||||
|
||||
> To be filled on completion
|
||||
70
tasks/config/dynamic-config.md
Normal file
70
tasks/config/dynamic-config.md
Normal file
@@ -0,0 +1,70 @@
|
||||
---
|
||||
id: config/dynamic-config
|
||||
name: Implement DynamicConfig with ArcSwap hot-reload and ConfigReloadHandle
|
||||
status: pending
|
||||
depends_on: [config/static-config]
|
||||
scope: moderate
|
||||
risk: medium
|
||||
impact: component
|
||||
level: implementation
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Implement the dynamic configuration that can be hot-reloaded at runtime without restarting the process. This is the core of the config reload mechanism.
|
||||
|
||||
### DynamicConfig
|
||||
|
||||
Hot-reloadable at runtime via `ArcSwap`. Changes take effect for new connections immediately.
|
||||
|
||||
- `sites: Vec<SiteConfig>` — hostname → upstream mapping (collected from all listeners)
|
||||
- `rate_limit: RateLimitConfig` — `requests_per_second: u32`, `burst: u32`
|
||||
- `body_limit_bytes: u64` — max request body size
|
||||
|
||||
**RateLimitConfig**:
|
||||
- `requests_per_second: u32` — required, > 0
|
||||
- `burst: u32` — required, > 0
|
||||
|
||||
### ArcSwap Pattern
|
||||
|
||||
- `Arc<ArcSwap<DynamicConfig>>` provides lock-free reads on the request hot path
|
||||
- `ConfigReloadHandle` with `reload(new_config)` method atomically swaps the entire config
|
||||
- No partial updates — the entire DynamicConfig is swapped at once
|
||||
- All request handlers read current config via `Arc` dereference (no lock contention)
|
||||
|
||||
### Reload Flow
|
||||
|
||||
1. Read the TOML config file from disk
|
||||
2. Deserialize into full config (both static and dynamic portions)
|
||||
3. Validate the full config (catches static misconfigurations early)
|
||||
4. If valid, swap DynamicConfig via ArcSwap; log warnings for any static changes
|
||||
5. If invalid, reject the reload and keep the old DynamicConfig
|
||||
|
||||
### Reload Serialization
|
||||
|
||||
Use `tokio::sync::Mutex` on the reload code path. If a reload is in progress and a second is requested, the second waits, re-reads the config file (getting the latest), then proceeds.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `DynamicConfig` struct defined with `sites`, `rate_limit`, and `body_limit_bytes` fields
|
||||
- [ ] `RateLimitConfig` struct defined with `requests_per_second` and `burst`
|
||||
- [ ] `Arc<ArcSwap<DynamicConfig>>` used for lock-free reads in handlers
|
||||
- [ ] `ConfigReloadHandle` struct with `reload(DynamicConfig)` method
|
||||
- [ ] Reload serialization via `tokio::sync::Mutex` prevents concurrent reload race conditions
|
||||
- [ ] Static config change detection: if static fields differ from current, log warning listing changed fields
|
||||
- [ ] Unit tests for ArcSwap swap (verify new config visible after reload)
|
||||
- [ ] Unit tests for reload rejection on invalid config
|
||||
- [ ] Unit tests for concurrent reload serialization
|
||||
|
||||
## References
|
||||
|
||||
- docs/architecture/config.md — DynamicConfig, ArcSwap pattern, reload flow
|
||||
- docs/architecture/decisions/008-static-dynamic-config-split.md — ArcSwap rationale
|
||||
|
||||
## Notes
|
||||
|
||||
> The sites vector is collected from all listeners into a single global routing table. Hostname uniqueness validation happens in the validation step, not in DynamicConfig itself.
|
||||
|
||||
## Summary
|
||||
|
||||
> To be filled on completion
|
||||
74
tasks/config/static-config.md
Normal file
74
tasks/config/static-config.md
Normal file
@@ -0,0 +1,74 @@
|
||||
---
|
||||
id: config/static-config
|
||||
name: Implement StaticConfig, ListenerConfig, TlsConfig, and LoggingConfig structs with TOML deserialization
|
||||
status: pending
|
||||
depends_on: [setup/project-init]
|
||||
scope: moderate
|
||||
risk: low
|
||||
impact: component
|
||||
level: implementation
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Implement the static configuration structs that are immutable after startup. These are deserialized from the TOML config file and validated at startup. Changes to static config require a process restart.
|
||||
|
||||
### Structs to Implement
|
||||
|
||||
**StaticConfig** (top-level, immutable after startup):
|
||||
- `listeners: Vec<ListenerConfig>` — at least one required
|
||||
- `allow_wildcard_bind: bool` — default `false`
|
||||
- `health_check_port: u16` — default `9900`, `0` to disable
|
||||
- `admin_socket_path: String` — default `/run/reverse-proxy/admin.sock`, empty string to disable
|
||||
- `shutdown_timeout_secs: u64` — default `30`
|
||||
- `logging: LoggingConfig`
|
||||
|
||||
**LoggingConfig** (nested in `[logging]`):
|
||||
- `level: String` — default `"info"`
|
||||
- `format: String` — default `"text"`
|
||||
- `log_file_path: Option<String>` — optional, enables file logging when set
|
||||
|
||||
**ListenerConfig** (per `[[listeners]]`):
|
||||
- `bind_addr: String` — required
|
||||
- `http_port: u16` — default `80`, `0` to disable
|
||||
- `https_port: u16` — default `443`
|
||||
- `tls: TlsConfig`
|
||||
- `sites: Vec<SiteConfig>` — sites defined per listener (moved to global routing in DynamicConfig)
|
||||
|
||||
**TlsConfig** (nested in `[listeners.tls]`):
|
||||
- `mode: String` — `"acme"` or `"manual"`
|
||||
- ACME fields: `acme_domains`, `acme_cache_dir`, `acme_directory`
|
||||
- Manual fields: `cert_path`, `key_path`
|
||||
|
||||
**SiteConfig** (per `[[listeners.sites]]`):
|
||||
- `host: String` — hostname to match
|
||||
- `upstream: String` — `host:port` format
|
||||
- `upstream_scheme: String` — default `"http"`
|
||||
- `upstream_connect_timeout_secs: u64` — default `5`
|
||||
- `upstream_request_timeout_secs: u64` — default `60`
|
||||
|
||||
All structs derive `Debug`, `Clone`, `serde::Deserialize`. Use serde defaults for optional fields.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `StaticConfig`, `LoggingConfig`, `ListenerConfig`, `TlsConfig`, and `SiteConfig` structs defined with correct fields and types
|
||||
- [ ] All structs derive `Debug`, `Clone`, `serde::Deserialize`
|
||||
- [ ] Default values implemented per config.md defaults table
|
||||
- [ ] TOML deserialization works for both multi-config (dedicated-IP) and shared-IP (SAN certificate) config formats
|
||||
- [ ] Unit tests verify deserialization of both example configs from config.md
|
||||
- [ ] `cargo check` and `cargo test` succeed
|
||||
|
||||
## References
|
||||
|
||||
- docs/architecture/config.md — full config structure, defaults, TOML format
|
||||
- docs/architecture/decisions/003-toml-config.md — TOML format decision
|
||||
- docs/architecture/decisions/008-static-dynamic-config-split.md — static/dynamic split rationale
|
||||
- docs/architecture/decisions/019-multi-config-listeners.md — `[[listeners]]` format
|
||||
|
||||
## Notes
|
||||
|
||||
> SiteConfig is defined per-listener in TOML but collected into a global routing table in DynamicConfig. The per-listener definition is just for config organization; at runtime, hostnames must be unique across all listeners.
|
||||
|
||||
## Summary
|
||||
|
||||
> To be filled on completion
|
||||
72
tasks/config/validation.md
Normal file
72
tasks/config/validation.md
Normal file
@@ -0,0 +1,72 @@
|
||||
---
|
||||
id: config/validation
|
||||
name: Implement config validation with all 18 validation rules and error reporting
|
||||
status: pending
|
||||
depends_on: [config/static-config]
|
||||
scope: moderate
|
||||
risk: medium
|
||||
impact: component
|
||||
level: implementation
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Implement comprehensive config validation per the 18 rules defined in config.md. Validation runs on startup (fail-fast, exit with non-zero code) and on reload (reject reload, log error).
|
||||
|
||||
### Validation Rules (from config.md)
|
||||
|
||||
1. At least one `[[listeners]]` entry must exist
|
||||
2. Each listener's `bind_addr` is not `0.0.0.0` unless `allow_wildcard_bind` is enabled (config OR CLI flag — OR relationship)
|
||||
3. Each listener's `bind_addr` and `https_port` combination must be unique
|
||||
4. In ACME mode, `acme_domains` must be non-empty
|
||||
5. In manual mode, `cert_path` and `key_path` must both be set and files must be readable
|
||||
6. Each site must have a `host` and `upstream`
|
||||
7. Site `host` values must be unique across all listeners (no duplicate hostnames)
|
||||
8. `rate_limit.requests_per_second` must be > 0
|
||||
9. `body.limit_bytes` must be > 0
|
||||
10. Each listener's `bind_addr` and `http_port` combination must be unique (if http_port > 0)
|
||||
11. Within a listener, `http_port` and `https_port` must differ
|
||||
12. `https_port` must be 1–65535 (required — TLS needs a port)
|
||||
13. `http_port` must be 0 (disabled) or 1–65535
|
||||
14. `health_check_port` must not conflict with any listener's `http_port` or `https_port` on the same bind address
|
||||
15. Site `host` values must not include a port number (e.g., `git.alk.dev`, not `git.alk.dev:443`)
|
||||
16. Site `host` values must be valid hostnames (not IP addresses, not including ports). Hostnames normalized to lowercase
|
||||
17. `upstream` must be in `host:port` format where `port` is 1–65535
|
||||
18. `upstream_scheme` values must be `"http"` or `"https"` (lowercase)
|
||||
|
||||
### Error Reporting
|
||||
|
||||
On validation failure, collect ALL errors (don't stop at first) and report them together. This helps operators fix multiple issues in one pass. Use a `Vec<ValidationError>` that is logged or printed on startup failure.
|
||||
|
||||
### Startup vs Reload Behavior
|
||||
|
||||
- **Startup**: If validation fails, exit with non-zero code and log all validation errors
|
||||
- **Reload**: If validation fails, reject the reload, log all errors, keep old DynamicConfig active
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] All 18 validation rules implemented
|
||||
- [ ] Validation collects all errors (doesn't stop at first)
|
||||
- [ ] `ValidationError` enum with descriptive messages for each rule
|
||||
- [ ] `validate(config: &StaticConfig, dynamic: &DynamicConfig) -> Result<(), Vec<ValidationError>>` function
|
||||
- [ ] Startup validation: exits with code 1 on failure, logs all errors
|
||||
- [ ] Reload validation: rejects reload on failure, logs all errors, keeps old config
|
||||
- [ ] `allow_wildcard_bind` OR logic: config flag OR CLI flag enables it
|
||||
- [ ] Hostname normalization to lowercase during validation
|
||||
- [ ] File existence check for manual mode `cert_path` and `key_path`
|
||||
- [ ] Unit tests covering each validation rule with valid and invalid inputs
|
||||
- [ ] Integration test: valid config from config.md examples passes all validation
|
||||
|
||||
## References
|
||||
|
||||
- docs/architecture/config.md — full validation rules, default values, TOML format
|
||||
- docs/architecture/decisions/016-explicit-bind-address.md — `0.0.0.0` rejection rationale
|
||||
- docs/architecture/decisions/020-container-deployment.md — `allow_wildcard_bind` for containers
|
||||
|
||||
## Notes
|
||||
|
||||
> Rule 5 (file readability check for manual certs) should check that the files exist and are readable at validation time, not just that the paths are set. This provides early feedback on misconfiguration.
|
||||
|
||||
## Summary
|
||||
|
||||
> To be filled on completion
|
||||
Reference in New Issue
Block a user