Some checks failed
Run Check Script / check (pull_request) Failing after 43s
108 lines
5.1 KiB
Markdown
108 lines
5.1 KiB
Markdown
# Configuration and Secrets
|
|
|
|
Harmony treats configuration and secrets as a single concern. Developers use one crate, `harmony_config`, to declare, store, and retrieve all runtime data — whether it is a public hostname or a database password.
|
|
|
|
## The mental model: schema in Git, state in the store
|
|
|
|
### Schema
|
|
|
|
In Harmony, the Rust code is the configuration schema. You declare what your module needs by defining a struct:
|
|
|
|
```rust
|
|
#[derive(Config, Serialize, Deserialize, JsonSchema, InteractiveParse)]
|
|
struct PostgresConfig {
|
|
pub host: String,
|
|
pub port: u16,
|
|
#[config(secret)]
|
|
pub password: String,
|
|
}
|
|
```
|
|
|
|
This struct is tracked in Git. When a branch adds a new field, Git tracks that the branch requires a new value. When a branch removes a field, the old value in the store becomes irrelevant. The struct is always authoritative.
|
|
|
|
### State
|
|
|
|
The actual values live in a config store — by default, OpenBao. No `.env` files, no JSON, no YAML in the repository.
|
|
|
|
When you run your code, Harmony reads the struct (schema) and resolves values from the store (state):
|
|
|
|
- If the store has the value, it is injected seamlessly.
|
|
- If the store does not have it, Harmony prompts you in the terminal. Your answer is pushed back to the store automatically.
|
|
- When a teammate runs the same code, they are not prompted — you already provided the value.
|
|
|
|
### How branch switching works
|
|
|
|
Because the schema is just Rust code tracked in Git, branch switching works naturally:
|
|
|
|
1. You check out `feat/redis`. The code now requires `RedisConfig`.
|
|
2. You run `cargo run`. Harmony detects that `RedisConfig` has no value in the store. It prompts you.
|
|
3. You provide the values. Harmony pushes them to OpenBao.
|
|
4. Your teammate checks out `feat/redis` and runs `cargo run`. No prompt — the values are already in the store.
|
|
5. You switch back to `main`. `RedisConfig` does not exist in that branch's code. The store entry is ignored.
|
|
|
|
## Secrets vs. standard configuration
|
|
|
|
From your application code, there is no difference. You always call `harmony_config::get_or_prompt::<T>()`.
|
|
|
|
The difference is in the struct definition:
|
|
|
|
```rust
|
|
// Standard config — stored in plaintext, displayed during prompting.
|
|
#[derive(Config)]
|
|
struct ClusterConfig {
|
|
pub api_url: String,
|
|
pub namespace: String,
|
|
}
|
|
|
|
// Contains a secret field — the entire struct is stored encrypted,
|
|
// and the password field is masked during terminal prompting.
|
|
#[derive(Config)]
|
|
struct DatabaseConfig {
|
|
pub host: String,
|
|
#[config(secret)]
|
|
pub password: String,
|
|
}
|
|
```
|
|
|
|
If a struct contains any `#[config(secret)]` field, Harmony elevates the entire struct to `ConfigClass::Secret`. The storage backend decides what that means in practice — in the case of OpenBao, it may route the data to a path with stricter ACLs or audit policies.
|
|
|
|
## Authentication and team sharing
|
|
|
|
Harmony uses Zitadel (hosted at `sso.nationtech.io`) for identity and OpenBao (hosted at `secrets.nationtech.io`) for storage.
|
|
|
|
**First run on a new machine:**
|
|
|
|
1. Harmony detects that you are not logged in.
|
|
2. It prints a short code and URL to your terminal, and opens your browser if possible.
|
|
3. You log in with your corporate identity (Google, GitHub, or Microsoft Entra ID / Azure AD).
|
|
4. Harmony receives an OIDC token, exchanges it for an OpenBao token, and caches the session locally.
|
|
|
|
**Subsequent runs:**
|
|
|
|
- Harmony silently refreshes your tokens in the background. You do not need to log in again for up to 90 days of active use.
|
|
- If you are inactive for 30 days, or if an administrator revokes your access in Zitadel, you will be prompted to re-authenticate.
|
|
|
|
**Offboarding:**
|
|
|
|
Revoking a user in Zitadel immediately invalidates their ability to refresh tokens or obtain new ones. No manual secret rotation is required.
|
|
|
|
## Resolution chain
|
|
|
|
When Harmony resolves a config value, it tries sources in order:
|
|
|
|
1. **Environment variable** (`HARMONY_CONFIG_{KEY}`) — highest priority. Use this in CI/CD to override any value without touching the store.
|
|
2. **Config store** (OpenBao for teams, local file for solo/offline use) — the primary source for shared team state.
|
|
3. **Interactive prompt** — last resort. Prompts the developer and persists the answer back to the store.
|
|
|
|
## Schema versioning
|
|
|
|
The Rust struct is the single source of truth for what configuration looks like. If a developer renames or removes a field on a branch, the store may still contain data shaped for the old version of the struct. When another developer who does not have that change runs the code, deserialization will fail.
|
|
|
|
In the current implementation, this is handled gracefully: a deserialization failure is treated as a miss, and Harmony re-prompts. The new answer overwrites the stale entry.
|
|
|
|
A compile-time migration mechanism is planned for a future release to handle this more rigorously at scale.
|
|
|
|
## Offline and local development
|
|
|
|
If you are working offline or evaluating Harmony without a team OpenBao instance, the `StoreSource` falls back to a local file store at `~/.local/share/harmony/config/`. The developer experience is identical — prompting, caching, and resolution all work the same way. The only difference is that the state is local to your machine and not shared with teammates.
|