Some checks failed
Run Check Script / check (pull_request) Failing after 12s
171 lines
6.7 KiB
Markdown
171 lines
6.7 KiB
Markdown
# Phase 1: Harden `harmony_config`, Validate UX, Zero-Setup Starting Point
|
|
|
|
## Goal
|
|
|
|
Make `harmony_config` production-ready with a seamless first-run experience: clone, run, get prompted, values persist locally. Then progressively add team-scale backends (OpenBao, Zitadel SSO) without changing any calling code.
|
|
|
|
## Current State
|
|
|
|
`harmony_config` now has:
|
|
|
|
- `Config` trait + `#[derive(Config)]` macro
|
|
- `ConfigManager` with ordered source chain
|
|
- Five `ConfigSource` implementations:
|
|
- `EnvSource` — reads `HARMONY_CONFIG_{KEY}` env vars
|
|
- `LocalFileSource` — reads/writes `{key}.json` files from a directory
|
|
- `SqliteSource` — **NEW** reads/writes to SQLite database
|
|
- `PromptSource` — returns `None` / no-op on set (placeholder for TUI integration)
|
|
- `StoreSource<S: SecretStore>` — wraps any `harmony_secret::SecretStore` backend
|
|
- 24 unit tests (mock source, env, local file, sqlite, prompt, integration)
|
|
- Global `CONFIG_MANAGER` static with `init()`, `get()`, `get_or_prompt()`, `set()`
|
|
- Two examples: `basic` and `prompting` in `harmony_config/examples/`
|
|
- **Zero workspace consumers** — nothing calls `harmony_config` yet
|
|
|
|
## Tasks
|
|
|
|
### 1.1 Add `SqliteSource` as the default zero-setup backend ✅
|
|
|
|
**Status**: Implemented
|
|
|
|
**Implementation Details**:
|
|
|
|
- Database location: `~/.local/share/harmony/config/config.db` (directory is auto-created)
|
|
- Schema: `config(key TEXT PRIMARY KEY, value TEXT NOT NULL, updated_at TEXT NOT NULL DEFAULT (datetime('now')))`
|
|
- Uses `sqlx` with SQLite runtime
|
|
- `SqliteSource::open(path)` - opens/creates database at given path
|
|
- `SqliteSource::default()` - uses default Harmony data directory
|
|
|
|
**Files**:
|
|
- `harmony_config/src/source/sqlite.rs` - new file
|
|
- `harmony_config/Cargo.toml` - added `sqlx = { workspace = true, features = ["runtime-tokio", "sqlite"] }`
|
|
- `Cargo.toml` - added `anyhow = "1.0"` to workspace dependencies
|
|
|
|
**Tests** (all passing):
|
|
- `test_sqlite_set_and_get` — round-trip a `TestConfig` struct
|
|
- `test_sqlite_get_returns_none_when_missing` — key not in DB
|
|
- `test_sqlite_overwrites_on_set` — set twice, get returns latest
|
|
- `test_sqlite_concurrent_access` — two tasks writing different keys simultaneously
|
|
|
|
### 1.1.1 Add Config example to show exact DX and confirm functionality ✅
|
|
|
|
**Status**: Implemented
|
|
|
|
**Examples created**:
|
|
|
|
1. `harmony_config/examples/basic.rs` - demonstrates:
|
|
- Zero-setup SQLite backend (auto-creates directory)
|
|
- Using the `#[derive(Config)]` macro
|
|
- Environment variable override (`HARMONY_CONFIG_TestConfig` overrides SQLite)
|
|
- Direct set/get operations
|
|
- Persistence verification
|
|
|
|
2. `harmony_config/examples/prompting.rs` - demonstrates:
|
|
- Config with no defaults (requires user input via `inquire`)
|
|
- `get()` flow: env > sqlite > prompt fallback
|
|
- `get_or_prompt()` for interactive configuration
|
|
- Full resolution chain
|
|
- Persistence of prompted values
|
|
|
|
### 1.2 Make `PromptSource` functional ✅
|
|
|
|
**Status**: Implemented with design improvement
|
|
|
|
**Key Finding - Bug Fixed During Implementation**:
|
|
|
|
The original design had a critical bug in `get_or_prompt()`:
|
|
```rust
|
|
// OLD (BUGGY) - breaks on first source where set() returns Ok(())
|
|
for source in &self.sources {
|
|
if source.set(T::KEY, &value).await.is_ok() {
|
|
break;
|
|
}
|
|
}
|
|
```
|
|
|
|
Since `EnvSource.set()` returns `Ok(())` (successfully sets env var), the loop would break immediately and never write to `SqliteSource`. Prompted values were never persisted!
|
|
|
|
**Solution - Added `should_persist()` method to ConfigSource trait**:
|
|
|
|
```rust
|
|
#[async_trait]
|
|
pub trait ConfigSource: Send + Sync {
|
|
async fn get(&self, key: &str) -> Result<Option<serde_json::Value>, ConfigError>;
|
|
async fn set(&self, key: &str, value: &serde_json::Value) -> Result<(), ConfigError>;
|
|
fn should_persist(&self) -> bool {
|
|
true
|
|
}
|
|
}
|
|
```
|
|
|
|
- `EnvSource::should_persist()` returns `false` - shouldn't persist prompted values to env vars
|
|
- `PromptSource::should_persist()` returns `false` - doesn't persist anyway
|
|
- `get_or_prompt()` now skips sources where `should_persist()` is `false`
|
|
|
|
**Updated `get_or_prompt()`**:
|
|
```rust
|
|
for source in &self.sources {
|
|
if !source.should_persist() {
|
|
continue;
|
|
}
|
|
if source.set(T::KEY, &value).await.is_ok() {
|
|
break;
|
|
}
|
|
}
|
|
```
|
|
|
|
**Tests**:
|
|
- `test_prompt_source_always_returns_none`
|
|
- `test_prompt_source_set_is_noop`
|
|
- `test_prompt_source_does_not_persist`
|
|
- `test_full_chain_with_prompt_source_falls_through_to_prompt`
|
|
|
|
### 1.3 Integration test: full resolution chain ✅
|
|
|
|
**Status**: Implemented
|
|
|
|
**Tests**:
|
|
- `test_full_resolution_chain_sqlite_fallback` — env not set, sqlite has value, get() returns sqlite
|
|
- `test_full_resolution_chain_env_overrides_sqlite` — env set, sqlite has value, get() returns env
|
|
- `test_branch_switching_scenario_deserialization_error` — old struct shape in sqlite returns Deserialization error
|
|
|
|
### 1.4 Validate Zitadel + OpenBao integration path ⏳
|
|
|
|
**Status**: Not yet implemented
|
|
|
|
Remaining work:
|
|
- Validate that `ConfigManager::new(vec![EnvSource, SqliteSource, StoreSource<Openbao>])` compiles
|
|
- When OpenBao is unreachable, chain falls through to SQLite gracefully
|
|
- Document target Zitadel OIDC flow as ADR
|
|
|
|
### 1.5 UX validation checklist ⏳
|
|
|
|
**Status**: Partially complete - manual verification needed
|
|
|
|
- [ ] `cargo run --example postgresql` with no env vars → prompts for nothing
|
|
- [ ] An example that uses `SecretManager` today (e.g., `brocade_snmp_server`) → when migrated to `harmony_config`, first run prompts, second run reads from SQLite
|
|
- [ ] Setting `HARMONY_CONFIG_BrocadeSwitchAuth='{"host":"...","user":"...","password":"..."}'` → skips prompt, uses env value
|
|
- [ ] Deleting `~/.local/share/harmony/config/` directory → re-prompts on next run
|
|
|
|
## Deliverables
|
|
|
|
- [x] `SqliteSource` implementation with tests
|
|
- [x] Functional `PromptSource` with `should_persist()` design
|
|
- [x] Fix `get_or_prompt` to persist to first writable source (via `should_persist()`), not all sources
|
|
- [x] Integration tests for full resolution chain
|
|
- [x] Branch-switching deserialization failure test
|
|
- [ ] `StoreSource<OpenbaoSecretStore>` integration validated (compiles, graceful fallback)
|
|
- [ ] ADR for Zitadel OIDC target architecture
|
|
- [ ] Update docs to reflect final implementation and behavior
|
|
|
|
## Key Implementation Notes
|
|
|
|
1. **SQLite path**: `~/.local/share/harmony/config/config.db` (not `~/.local/share/harmony/config.db`)
|
|
|
|
2. **Auto-create directory**: `SqliteSource::open()` creates parent directories if they don't exist
|
|
|
|
3. **Default path**: `SqliteSource::default()` uses `directories::ProjectDirs` to find the correct data directory
|
|
|
|
4. **Env var precedence**: Environment variables always take precedence over SQLite in the resolution chain
|
|
|
|
5. **Testing**: All tests use `tempfile::NamedTempFile` for temporary database paths, ensuring test isolation
|