Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,19 @@ Multi-environment configurations are managed using the `--profile` (`-p`) flag:

---

## ⚙️ Project Configuration File (`kaji.toml` / `.kaji.toml`)

Project connection defaults and workspace parameters can be declared inside `kaji.toml` or `.kaji.toml` files in the current working directory. The configuration file is parsed at startup and merged with command-line inputs.

Settings are resolved in the following precedence order:
1. **CLI Flags** (highest)
2. **Profile Configuration**
3. **Environment Variables**
4. **TOML Configuration**
5. **Fallback Defaults** (lowest)

---

## 🔐 Secret Management & Resolution

Secrets are managed via the `SecretResolver` trait using three resolution strategies:
Expand Down
60 changes: 60 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ sanitize-filename = "0.6.0"
serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.149"
serde_yaml = { package = "serde_yaml_ng", version = "0.10.0" }
toml = "0.8.20"
tokio = { version = "1.49.0", features = ["full"] }
similar = "2.7.0"
async-trait = "0.1.86"
Expand Down Expand Up @@ -58,3 +59,6 @@ harness = false
[[bench]]
name = "models_bench"
harness = false

[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(tarpaulin_include)', 'cfg(tarpaulin)'] }
15 changes: 15 additions & 0 deletions GEMINI.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,21 @@ For any resource `resource.yaml`, `kaji` looks for `resource.{profile}.yaml` and

---

## ⚙️ Project-Level Configuration File (`kaji.toml` / `.kaji.toml`)

`kaji` supports project-level settings to override defaults and connection variables persistently.

### Architecture & Pipeline
1. **Schema Definition**: The `Config` struct is defined in [src/args.rs](src/args.rs). It represents optional fields for Keycloak connection credentials, vault parameters, and the workspace folder.
2. **File Lookup**: In [src/lib.rs](src/lib.rs), `load_config_file` checks for:
* A custom config path specified via `--config` CLI flag or `KAJI_CONFIG` env var.
* `kaji.toml` in the current working directory.
* `.kaji.toml` in the current working directory.
3. **Merging Logic**: In `run_app` in `src/lib.rs`, the loaded `Config` is merged into the parsed CLI `Cli` struct. Settings are resolved using a strictly defined precedence logic:
* **CLI Flags** > **Active Profile** > **Environment Variables** > **TOML Configuration** > **Default Fallbacks**.

---

## 🛠️ Adding a New Resource Support

To support a new Keycloak resource (e.g., "Event Listeners"):
Expand Down
37 changes: 37 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,43 @@ When running with `--profile prod`, `kaji` deep-merges the overlay onto the base
| `VAULT_ADDR` | HashiCorp Vault URL | |
| `VAULT_TOKEN` | HashiCorp Vault Token | |

### Configuration File (`kaji.toml` / `.kaji.toml`)

`kaji` supports persisting connection and workspace settings in a TOML configuration file. By default, it searches for `kaji.toml` and then `.kaji.toml` in the current working directory. You can also specify a custom configuration file path using the global `--config` flag or the `KAJI_CONFIG` environment variable.

#### Example: `kaji.toml`
```toml
# Keycloak Server URL
server = "http://localhost:8080"

# Target realms (if empty, all realms are considered)
realms = ["master", "myrealm"]

# Admin User
user = "admin"

# Client ID used for auth
client_id = "kaji-cli"

# Default environment profile to load
profile = "dev"

# Workspace directory containing configuration files (defaults to "workspace")
workspace = "kaji-workspace"

# HashiCorp Vault Settings
vault_addr = "http://vault:8200"
vault_token = "my-vault-token"
```

#### Precedence / Priority Rules
When resolving settings, `kaji` merges settings from different sources in the following priority order (highest to lowest):
1. **CLI Flags** (explicitly passed on command line, e.g. `--server` or `--workspace`)
2. **Profile Configuration** (loaded from `profiles/` directory when `--profile` or `profile` is specified)
3. **Environment Variables** (e.g. `KEYCLOAK_URL`, `KEYCLOAK_CLIENT_ID`)
4. **Config File** (`kaji.toml` / `.kaji.toml`)
5. **Fallback Defaults** (e.g., `"admin-cli"` for client ID, `"workspace"` for workspace)

### Workspace Structure

```text
Expand Down
8 changes: 4 additions & 4 deletions docs/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -154,12 +154,12 @@ <h3>Dependency-Aware Reconciliation</h3>

<article class="feature-card" id="feat-profiles">
<div class="feature-icon-wrap"><span class="feature-icon"><svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg></span></div>
<h3>Environment Profiles &amp; Overlays</h3>
<p>One base configuration, zero duplication. Override only what differs per environment via YAML deep-merge overlays.</p>
<h3>Profiles &amp; TOML Config</h3>
<p>Persist connection credentials, workspace paths, and target environments cleanly. Support for profiles, overlays, and project-wide configuration files.</p>
<ul class="feature-list">
<li><span class="check">&#x2713;</span> kaji.toml / .kaji.toml project config</li>
<li><span class="check">&#x2713;</span> profiles/dev.yaml, staging.yaml, prod.yaml</li>
<li><span class="check">&#x2713;</span> YAML deep-merge overlays per resource</li>
<li><span class="check">&#x2713;</span> Per-profile secrets files</li>
<li><span class="check">&#x2713;</span> YAML overlays deep-merged at runtime</li>
</ul>
</article>

Expand Down
61 changes: 43 additions & 18 deletions src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ pub struct Cli {
pub command: Commands,

/// Keycloak Server URL
#[arg(long, env = "KEYCLOAK_URL", required_unless_present = "profile")]
#[arg(long, env = "KEYCLOAK_URL")]
pub server: Option<String>,

/// Keycloak Realms to consider. If empty, all realms are considered.
Expand All @@ -27,8 +27,8 @@ pub struct Cli {
pub password: Option<String>,

/// Keycloak Client ID (for client credentials grant)
#[arg(long, env = "KEYCLOAK_CLIENT_ID", default_value = "admin-cli")]
pub client_id: String,
#[arg(long, env = "KEYCLOAK_CLIENT_ID")]
pub client_id: Option<String>,

/// Keycloak Client Secret (for client credentials grant)
#[arg(skip)]
Expand All @@ -45,6 +45,10 @@ pub struct Cli {
/// HashiCorp Vault Token
#[arg(long, env = "VAULT_TOKEN")]
pub vault_token: Option<String>,

/// Path to a custom TOML configuration file
#[arg(long, env = "KAJI_CONFIG")]
pub config: Option<PathBuf>,
}

impl fmt::Debug for Cli {
Expand All @@ -71,13 +75,13 @@ impl fmt::Debug for Cli {
}

/// List of subcommands supported by `kaji`.
#[derive(Subcommand, Debug)]
#[derive(Subcommand, Debug, Clone)]
pub enum Commands {
/// Inspect the current Keycloak configuration and dump to files
Inspect {
/// Workspace directory for configuration files
#[arg(long, short = 'w', default_value = "workspace")]
workspace: PathBuf,
#[arg(long, short = 'w')]
workspace: Option<PathBuf>,

/// Skip confirmation prompt when overwriting local files
#[arg(long, short = 'y', default_value = "false")]
Expand All @@ -86,14 +90,14 @@ pub enum Commands {
/// Validate the local Keycloak configuration files
Validate {
/// Workspace directory containing configuration files
#[arg(long, short = 'w', default_value = "workspace")]
workspace: PathBuf,
#[arg(long, short = 'w')]
workspace: Option<PathBuf>,
},
/// Apply the local Keycloak configuration to the server
Apply {
/// Workspace directory containing configuration files
#[arg(long, short = 'w', default_value = "workspace")]
workspace: PathBuf,
#[arg(long, short = 'w')]
workspace: Option<PathBuf>,

/// Skip confirmation prompt
#[arg(long, short = 'y', default_value = "false")]
Expand All @@ -106,8 +110,8 @@ pub enum Commands {
/// Plan the application of the local Keycloak configuration
Plan {
/// Workspace directory containing configuration files
#[arg(long, short = 'w', default_value = "workspace")]
workspace: PathBuf,
#[arg(long, short = 'w')]
workspace: Option<PathBuf>,

/// Show only changes, suppressing "No changes" messages
#[arg(long, short = 'c')]
Expand All @@ -120,23 +124,44 @@ pub enum Commands {
/// Check for drift between local configuration and server
Drift {
/// Workspace directory containing configuration files
#[arg(long, short = 'w', default_value = "workspace")]
workspace: PathBuf,
#[arg(long, short = 'w')]
workspace: Option<PathBuf>,
},
/// Interactive CLI mode to generate local configuration
Cli {
/// Workspace directory for configuration files
#[arg(long, short = 'w', default_value = "workspace")]
workspace: PathBuf,
#[arg(long, short = 'w')]
workspace: Option<PathBuf>,
},
/// Clean the local configuration files
Clean {
/// Workspace directory containing configuration files
#[arg(long, short = 'w', default_value = "workspace")]
workspace: PathBuf,
#[arg(long, short = 'w')]
workspace: Option<PathBuf>,

/// Skip confirmation prompt
#[arg(long, short = 'y', default_value = "false")]
yes: bool,
},
}

/// The schema of `.kaji.toml` / `kaji.toml` configuration file.
#[derive(serde::Deserialize, Debug, Default, Clone)]
pub struct Config {
/// Keycloak Server URL
pub server: Option<String>,
/// Keycloak Realms to steer
pub realms: Option<Vec<String>>,
/// Keycloak Admin User
pub user: Option<String>,
/// Keycloak Client ID
pub client_id: Option<String>,
/// Environment Profile Name
pub profile: Option<String>,
/// HashiCorp Vault URL
pub vault_addr: Option<String>,
/// HashiCorp Vault Token
pub vault_token: Option<String>,
/// Workspace directory
pub workspace: Option<PathBuf>,
}
Loading