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
15 changes: 12 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,15 @@ All business logic is located in the `src/` directory:
* [`src/client.rs`](src/client.rs): Wrapper around the Keycloak Admin REST API. Handles authentication and provides a **generic CRUD interface** for resources.
* [`src/models.rs`](src/models.rs): Strongly-typed Serde representations of Keycloak resources. Implements the `KeycloakResource` and `ResourceMeta` traits.
* [`src/inspect.rs`](src/inspect.rs): Scans the remote Keycloak instance and serializes resources into local workspace files using a parallelized pipeline.
* [`src/plan/`](src/plan/): Calculates diffs and writes the plan. Uses the generic planning engine in `generic.rs`.
* [`src/plan/`](src/plan/): Calculates diffs and writes the plan. Uses the generic planning engine in `generic.rs`. Supports collapsed unified diff formatting (3 context lines) by default, `--verbose` full diff view, and interactive expansion choices during confirmation.
* [`src/apply/`](src/apply/): Reconciles resources. Uses the generic reconciliation engine in `generic.rs` and stage-specific modules.
* [`src/validate.rs`](src/validate.rs): Validates local configurations against expected structures and constraints.
* [`src/clean.rs`](src/clean.rs): Removes unreferenced or invalid configuration files from the workspace.
* [`src/cli/`](src/cli/): Interactive CLI scaffolding menu.
* [`src/init.rs`](src/init.rs): Scaffolds the initial `kaji.toml` / `.kaji.toml` configuration files.
* [`src/cli/`](src/cli/): Interactive CLI scaffolding menu. Styled with `dialoguer`'s `ColorfulTheme` and uses `FuzzySelect` for real-time query filtering. Auto-discovers existing realms in the workspace directory.
* [`src/utils/secrets/`](src/utils/secrets/): Manages secret resolution (Env, HashiCorp Vault).
* [`src/utils/yaml.rs`](src/utils/yaml.rs): Handles YAML serialization, sorting, and profile-specific deep-merging.
* [`src/utils/ui.rs`](src/utils/ui.rs): CLI visual formatting, progress bars, emojis, and styling.
* [`src/utils/ui.rs`](src/utils/ui.rs): CLI visual formatting, progress bars, emojis, and styling. Contains DialoguerUi which maps console prompts to ColorfulTheme and FuzzySelect.

---

Expand All @@ -44,6 +45,14 @@ To prevent race conditions, resources are reconciled sequentially across stages:
| **Stage 2** | Clients, Client Scopes, Authentication Flows, Required Actions, Groups | Structure |
| **Stage 3** | Users, Authenticator Configs, Components, Keys | Data & Final Config |

## 🔄 Keycloak Resource Enrichment

During the reconciliation (`apply`) process, Keycloak may enrich resources with default values, read-only system attributes, or server-assigned identifiers (IDs). When `kaji` detects differences between the local representation and the enriched one returned by Keycloak:
1. It recursively maps any user-defined secret placeholders (e.g. `${VAR_NAME}`) from the original local file to the enriched representation to prevent them from being lost or overwritten by redacted/actual secret values.
2. It prompts the user (defaulting to Yes) to update the local representation to match the enriched Keycloak representation.
3. If the `--yes` (`-y`) option flag is passed, the update is accepted automatically without prompting.
4. Any newly generated secrets (such as client secrets) are extracted and appended to the secrets file.

---

## 🌍 Environment Profiles & Overlays
Expand Down
19 changes: 19 additions & 0 deletions Cargo.lock

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

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ similar = "2.7.0"
async-trait = "0.1.86"
async-recursion = "1.1.1"
console = "0.16.3"
dialoguer = "0.12.0"
dialoguer = { version = "0.12.0", features = ["fuzzy-select"] }
futures = "0.3.32"
indicatif = "0.18.4"

Expand Down
25 changes: 25 additions & 0 deletions GEMINI.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ This document serves as the internal developer guide for `kaji`. It explains the
- `src/utils/secrets/`: Manages secret resolution (Env, Vault, etc.).
- `src/utils/yaml.rs`: Handles YAML deep-merging and profile-specific overlays.
- `src/utils/ui.rs`: Centralized module for CLI output formatting, emoji management, and **indicatif progress bars**.
- `src/init.rs`: Scaffolds the initial `kaji.toml` / `.kaji.toml` configuration files.

---

Expand All @@ -33,6 +34,14 @@ To prevent race conditions and ensure correct dependency handling, `apply` is ex
3. **Stage 2**: Clients, Client Scopes, Authentication Flows, Required Actions, Groups (Structure).
4. **Stage 3**: Users, Authenticator Configs, Components, Keys (Data & Final Config).

## 🔄 Keycloak Resource Enrichment

During the reconciliation (`apply`) process, Keycloak may enrich resources with default values, read-only system attributes, or server-assigned identifiers (IDs). When `kaji` detects differences between the local representation and the enriched one returned by Keycloak:
1. It recursively maps any user-defined secret placeholders (e.g. `${VAR_NAME}`) from the original local file to the enriched representation to prevent them from being lost or overwritten by redacted/actual secret values.
2. It prompts the user (defaulting to Yes) to update the local representation to match the enriched Keycloak representation.
3. If the `--yes` (`-y`) option flag is passed, the update is accepted automatically without prompting.
4. Any newly generated secrets (such as client secrets) are extracted and appended to the secrets file.

---

## 🌍 Environment Profiles & Overlays
Expand Down Expand Up @@ -84,6 +93,22 @@ To support a new Keycloak resource (e.g., "Event Listeners"):

---

## 📺 Terminal UI & Diff Viewer Enhancements

### 1. Minimal Unified Diffs (Collapsed by Default)
To reduce terminal clutter, `kaji plan` and `kaji drift` default to showing collapsed unified diffs (with 3 context lines around changes). A `--verbose` or `-v` flag allows users to output full file diffs.

### 2. Interactive Diff Expansion
During `kaji plan --interactive`, the prompt is a selection menu with `Yes` (include change), `No` (skip change), and `Show Full Diff` (expand to full verbose diff). Selecting `Show Full Diff` displays the complete diff and prompts the user again.

### 3. Styled Fuzzy Scaffolding Menu
The interactive menu (`kaji cli`) utilizes `dialoguer::theme::ColorfulTheme` for polished, colorful CLI prompts. It replaces standard selects with `dialoguer::FuzzySelect`, allowing users to type to search and filter options instantly.

### 4. Workspace Realm Auto-Discovery
All scaffolding prompts dynamically scan the workspace to discover existing realms. The user is presented with a `FuzzySelect` list of discovered realms plus a `<Create New Realm...>` option, avoiding manual typing for existing projects.

---

## 🧪 Testing Strategy

`kaji` employs a multi-layered testing strategy:
Expand Down
1 change: 1 addition & 0 deletions JULES.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ Detailed guidelines are located under the `.jules/` directory:
## 🏗️ Architecture & Operations Summary

* **Diff Engine & Reconciler**: The workflow calculates differences between the desired local YAML state and the Keycloak Admin API state, generating a `.kajiplan` file, and applying it sequentially in stages (Stage 0: Realms; Stage 1: IDPs/Roles; Stage 2: Clients/Scopes/Flows/Groups; Stage 3: Users/Authenticator Configs/Components/Keys).
* **Project Bootstrapping**: Scaffold project configuration files (`kaji.toml` / `.kaji.toml`) using the `kaji init` command.
* **Overlay Deep Merging**: Keycloak resources can be customized per-environment by applying overlays (e.g. `roles/admin.prod.yaml` over `roles/admin.yaml`).
* **Documentation Rule**: Always update [README.md](README.md), [AGENTS.md](AGENTS.md), [JULES.md](JULES.md), and [.jules/](.jules/) files when changing the codebase.
* **Quality Commands**:
Expand Down
24 changes: 21 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ $ kaji cli
- **Declarative State**: Define your desired Keycloak state in human-readable YAML files.
- **Environment Profiles & Overlays**: Manage multiple environments (Dev, Staging, Prod) with zero configuration duplication.
- **Dependency-Aware Reconciliation**: Guaranteed correct application order through staged reconciliation (e.g., Realms -> Roles -> Users).
- **Bootstrapping & Scaffolding**: Easily initialize new project configurations (`kaji.toml` / `.kaji.toml`) with the `init` command.
- **Inspect & Export**: Bootstrap your project by exporting existing Keycloak configurations to local files.
- **Dry-Run Planning**: Preview exactly what changes will be applied with detailed diffs and summaries.
- **Interactive Review**: Confirm individual changes before they are applied to the server using the `--review` flag.
Expand Down Expand Up @@ -230,12 +231,16 @@ kaji validate
```

### `plan`
Calculates the "diff" between local files and the remote server.
Calculates the "diff" between local files and the remote server. By default, it shows a minimal, clean unified diff (collapsed with 3 lines of context).
```bash
# Plan for a specific profile
kaji plan --profile prod

# Interactive: decide for each change whether to include it in the plan
# Show full resource diffs instead of collapsed unified diffs
kaji plan --verbose

# Interactive: decide for each change whether to include it.
# Prompts you with: Yes (include), No (skip), Show Full Diff (to expand)
kaji plan --interactive
```

Expand All @@ -250,9 +255,12 @@ kaji apply --profile prod --review
```

### `drift`
A shortcut for `plan --changes-only`.
A shortcut for `plan --changes-only`. By default, it prints collapsed unified diffs.
```bash
kaji drift --profile prod

# Show full resource diffs for configuration drift
kaji drift --verbose
```

### `clean`
Expand All @@ -267,6 +275,16 @@ An interactive menu to generate resource scaffolds or perform quick actions.
kaji cli
```

### `init`
Scaffolds an initial `kaji.toml` / `.kaji.toml` project configuration. Automatically pre-fills settings from the environment where available.
```bash
# Non-interactive mode (uses environment variables to pre-fill, otherwise writes empty/default configuration)
kaji init

# Interactive mode (asks you for configuration parameters step-by-step)
kaji init --interactive
```

---

## 🔐 Secret Management
Expand Down
32 changes: 21 additions & 11 deletions docs/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -79,18 +79,19 @@ <h1 class="hero-title">
<span class="t-title">kaji plan</span>
</div>
<pre class="terminal-body"><code><span class="t-dim">$</span> <span class="t-cmd">kaji plan --interactive</span>
<span class="t-info">// Calculating diff for realm 'master'...</span>
<span class="t-info">🚀 Planning changes for realm: master</span>

<span class="t-section"> Clients:</span>
<span class="t-add"> [+] my-new-app</span> <span class="t-tag">Create</span>
<span class="t-mod"> [~] admin-cli</span> <span class="t-tag-mod">Update</span>
<span class="t-dim"> root_url: "http://localhost" &#x2192; "https://idp.example.com"</span>
<span class="t-del"> [-] legacy-app</span> <span class="t-tag-del">Delete</span>
<span class="t-section">📝 Changes for Client admin-cli:</span>
<span class="t-info">@@ -15,5 +15,5 @@</span>
<span class="t-dim"> protocol: openid-connect</span>
<span class="t-del">- rootUrl: "http://localhost"</span>
<span class="t-add">+ rootUrl: "https://idp.example.com"</span>
<span class="t-dim"> publicClient: false</span>

<span class="t-section"> Roles:</span>
<span class="t-add"> [+] billing-manager</span> <span class="t-tag">Create</span>

<span class="t-prompt">? Apply change to client 'my-new-app'?</span> <span class="t-cursor">&#x2588;</span></code></pre>
<span class="t-prompt">? Include this change in the plan?</span>
<span class="t-add">> Yes</span>
<span class="t-dim"> No</span>
<span class="t-dim"> Show Full Diff</span> <span class="t-cursor">&#x2588;</span></code></pre>
</div>
</div>
<div class="hero-scroll-hint" aria-hidden="true">
Expand Down Expand Up @@ -292,9 +293,18 @@ <h2 class="section-title">Up and running in <span class="gradient-text">seconds<
<div class="container">
<div class="section-header">
<span class="section-eyebrow">Command Reference</span>
<h2 class="section-title">Six commands to <span class="gradient-text">rule your realm</span></h2>
<h2 class="section-title">Seven commands to <span class="gradient-text">rule your realm</span></h2>
</div>
<div class="commands-grid">
<article class="command-card" id="cmd-init">
<div class="command-header">
<code class="command-name">kaji init</code>
<span class="command-badge command-badge--blue">Scaffold</span>
</div>
<p class="command-desc">Scaffold an initial kaji.toml project configuration. Prefills values from your environment variables automatically.</p>
<pre class="command-example"><code>kaji init --interactive</code></pre>
</article>

<article class="command-card" id="cmd-inspect">
<div class="command-header">
<code class="command-name">kaji inspect</code>
Expand Down
27 changes: 26 additions & 1 deletion src/apply/authenticator_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,14 @@ use tokio::fs as async_fs;
pub async fn apply_authenticator_configs(
client: &KeycloakClient,
workspace_dir: &std::path::Path,
secrets_path: Arc<PathBuf>,
resolver: Arc<dyn SecretResolver>,
planned_files: Arc<Option<HashSet<PathBuf>>>,
_realm_name: &str,
realm_name: &str,
profile: Option<String>,
review: bool,
ui: Arc<dyn Ui>,
yes: bool,
) -> Result<()> {
let resources_dir = workspace_dir.join(AuthenticatorConfigRepresentation::DIR_NAME);
if !async_fs::try_exists(&resources_dir).await? {
Expand Down Expand Up @@ -101,6 +103,7 @@ pub async fn apply_authenticator_configs(
// 3. Process each config
for path in files {
let mut val = load_yaml_with_overlay(&path, profile.as_deref()).await?;
let local_val_before_sub = val.clone();
substitute_secrets(&mut val, Arc::clone(&resolver)).await?;
let mut local_config: AuthenticatorConfigRepresentation = serde_json::from_value(val)
.with_context(|| format!("Failed to deserialize YAML file: {:?}", path))?;
Expand All @@ -110,6 +113,8 @@ pub async fn apply_authenticator_configs(
.clone()
.context("Config is missing 'alias'")?;

let final_id;

if let Some(remote) = remote_map.get(&alias) {
// Config exists! Update it
if review {
Expand All @@ -129,6 +134,7 @@ pub async fn apply_authenticator_configs(
" {} Updated authenticator config {}",
SUCCESS_UPDATE, alias
));
final_id = remote_id;
} else {
// New config! Create it
if review {
Expand Down Expand Up @@ -244,7 +250,26 @@ pub async fn apply_authenticator_configs(
}
}
}
final_id = new_config_id;
}

if let Ok(enriched) = client
.get_resource::<AuthenticatorConfigRepresentation>(&final_id)
.await
{
crate::apply::generic::check_and_update_enrichment(
client,
&path,
&local_val_before_sub,
&enriched,
realm_name,
&secrets_path,
&*ui,
yes,
)
.await?;
}

pb.inc(1);
}
pb.finish_with_message("Applied authenticator configs");
Expand Down
Loading