# Ilum API

REST API for Ilum module management — wraps the CLI's `ReleaseManager` to expose module, values, and operations management over HTTP.

---

## Overview

The `ilum.api` package is a FastAPI service that sits between the Ilum UI and the CLI's core logic. Instead of shelling out to the `ilum` CLI, the UI calls these endpoints to list modules, enable/disable them, inspect values, and track long-running operations. The API reuses the same `ReleaseManager`, `ModuleResolver`, `HelmClient`, and `KubeClient` that power the CLI — no logic is duplicated.

---

## Quick Start

```bash
# Install with API extras
pip install -e ".[dev]"

# Run with defaults (0.0.0.0:8080)
ilum-api

# Or with environment overrides
ILUM_API_HOST=127.0.0.1 ILUM_API_PORT=9000 ILUM_API_KEY=secret ilum-api

# Or via uvicorn directly
uvicorn ilum.api.app:create_app --factory --host 0.0.0.0 --port 8080
```

On startup the API auto-connects to a release: it reads `ILUM_RELEASE_NAME` / `ILUM_NAMESPACE` from the environment, or falls back to scanning for Helm releases in the cluster.

---

## Architecture

### Package Layout

```
src/ilum/api/
├── __init__.py          # Package marker
├── app.py               # FastAPI factory, router registration, lifespan hook
├── run.py               # Console script entry point (ilum-api)
├── startup.py           # Auto-connect: build ReleaseManager from env or scan
├── auth.py              # Triple auth (Nginx cookie, Bearer JWT, API key)
├── deps.py              # Singleton ReleaseManager dependency injection
├── errors.py            # IlumError → HTTP status mapping
├── models.py            # Pydantic v2 request/response schemas
├── operations.py        # In-memory operation store + background thread runner
└── routers/
    ├── status.py        # /health, /release, /pods
    ├── modules.py       # /modules CRUD + enable/disable
    ├── values.py        # /values get, diff, update
    └── operations.py    # /operations list + get
```

### Request Flow

```
Client → [Nginx proxy] → FastAPI
                            ├─ auth.py (require_auth)
                            ├─ deps.py (get_manager → ReleaseManager singleton)
                            ├─ router handler
                            │    ├─ sync reads → ReleaseManager → HelmClient/KubeClient
                            │    └─ async writes → OperationStore.run_in_thread()
                            └─ errors.py (IlumError → JSONResponse)
```

Mutating operations (enable, disable, values update) run in background threads and return `202 Accepted` with an operation ID. Clients poll `/api/v1/operations/{id}` for status.

---

## Endpoints

All endpoints are prefixed with `/api/v1`.

| Path | Method | Auth | Response Model | Status | Description |
|------|--------|------|----------------|--------|-------------|
| `/health` | GET | No | `HealthResponse` | 200 | Liveness/readiness probe |
| `/release` | GET | Yes | `ReleaseResponse` | 200 | Connected Helm release info |
| `/pods` | GET | Yes | `list[PodStatusResponse]` | 200 | All pods in release namespace |
| `/modules` | GET | Yes | `list[ModuleSummary]` | 200 | List modules (filter: `?category=`, `?enabled=`) |
| `/modules/{name}` | GET | Yes | `ModuleDetail` | 200 | Module detail with live pod health |
| `/modules/{name}/enable` | POST | Yes | `OperationCreated` | 202 | Enable module (async) |
| `/modules/{name}/disable` | POST | Yes | `OperationCreated` | 202 | Disable module (async) |
| `/values` | GET | Yes | `dict` | 200 | Current Helm values |
| `/values` | PUT | Yes | `OperationCreated` | 202 | Apply `--set` flags (async) |
| `/values/diff` | GET | Yes | `ValuesDiffResponse` | 200 | Drift detection vs CLI snapshot |
| `/operations` | GET | Yes | `list[OperationResponse]` | 200 | List recent operations (newest first) |
| `/operations/{op_id}` | GET | Yes | `OperationResponse` | 200 | Get operation status by ID |

---

## Authentication

The API supports three auth mechanisms (any one is sufficient):

| Method | Header | How It Works |
|--------|--------|--------------|
| Nginx cookie | `X-Ilum-Authenticated: true` | Set by Nginx after validating the user's session cookie. Used in production behind the Nginx proxy. |
| Bearer token | `Authorization: Bearer <token>` | JWT validated locally or forwarded to ilum-core (see below). Used for OAuth2/OIDC integrations. |
| API key | `X-API-Key: <key>` | Compared against `ILUM_API_KEY` env var. Used for direct/programmatic access. |

The `/health` endpoint is unauthenticated (used for k8s probes). All other endpoints require auth and return `401` on failure.

### Bearer Token Validation

Bearer tokens are validated in two paths, tried in order:

1. **Local JWT validation** — If `ILUM_JWT_PUBLIC_KEY` is set, the token is verified locally using RS256 with optional issuer (`ILUM_JWT_ISSUER_URI`) and audience (`ILUM_JWT_AUDIENCES`) checks.
2. **Forward to ilum-core** — If no public key is configured (or local validation fails), the token is forwarded to ilum-core's `/authenticate/set-permission-cookies` endpoint. A `200` response means the token is valid.

Validated results are cached in an in-memory LRU cache (max 1000 entries, TTL configurable via `ILUM_BEARER_CACHE_TTL`, default 60s) to avoid repeated HTTP calls for the same token.

If `ILUM_API_KEY` is not set, API key auth is effectively disabled. If `ILUM_JWT_PUBLIC_KEY` is not set and ilum-core is unreachable, Bearer auth is effectively disabled.

---

## Async Operations

Enable, disable, and values-update operations are long-running (they run `helm upgrade` under the hood). The API handles them asynchronously:

1. **Client sends request** → router validates input, creates a pending operation in `OperationStore`
2. **Returns `202 Accepted`** with `OperationCreated` (contains the operation `id`)
3. **Background thread** runs the actual Helm operation via `OperationStore.run_in_thread()`
4. **Client polls** `GET /api/v1/operations/{id}` until status is `completed` or `failed`

### Operation Status Lifecycle

```
pending → running → completed
                  → failed (with error + error_code)
```

The `OperationStore` is in-memory with a FIFO cap of 100 operations. Operations are not persisted across restarts.

---

## Error Handling

All `IlumError` subclasses are caught by a global exception handler and mapped to HTTP status codes:

| IlumError Subclass | HTTP Status | Meaning |
|--------------------|-------------|---------|
| `ModuleError` | 400 | Invalid module name, dependency conflict |
| `AuthError` | 401 | Authentication failure |
| `ReleaseNotFoundError` | 404 | Helm release not found |
| `ReleaseExistsError` | 409 | Release already exists |
| `ValuesError` | 422 | Invalid values / merge failure |
| `HelmError` | 502 | Helm command failed |
| `ClusterConnectionError` | 503 | Cannot reach Kubernetes cluster |
| `HelmTimeoutError` | 504 | Helm operation timed out |
| *(other IlumError)* | 500 | Unmapped error |

Error responses use the `ErrorResponse` schema:

```json
{
  "detail": "Module 'foo' not found in registry",
  "error_code": "ILUM-020",
  "suggestion": "Run 'ilum module list' to see available modules"
}
```

---

## Pydantic Models

Defined in `models.py`:

| Model | Used By | Purpose |
|-------|---------|---------|
| `HealthResponse` | `GET /health` | Status, release name, namespace |
| `ErrorResponse` | Exception handler | detail, error_code, suggestion |
| `ModuleSummary` | `GET /modules` | Compact module info for lists |
| `ModuleDetail` | `GET /modules/{name}` | Full info + pod health + deps |
| `PodInfo` | `ModuleDetail.pods` | Pod phase, ready, restart_count |
| `ReleaseResponse` | `GET /release` | Helm release metadata |
| `PodStatusResponse` | `GET /pods` | Pod info with containers list |
| `ValuesDiffEntry` | `ValuesDiffResponse.changes` | Single key change (added/removed/changed) |
| `ValuesDiffResponse` | `GET /values/diff` | Drift summary + change list |
| `ValuesUpdateRequest` | `PUT /values` | Request body: `set_flags` list |
| `OperationResponse` | `GET /operations` | Full operation status |
| `OperationCreated` | `POST` enable/disable/update | 202 response with op ID |

---

## Configuration

### Environment Variables

| Variable | Default | Description |
|----------|---------|-------------|
| `ILUM_API_HOST` | `0.0.0.0` | Bind address |
| `ILUM_API_PORT` | `8080` | Bind port |
| `ILUM_API_KEY` | *(empty)* | API key for `X-API-Key` auth. If unset, API key auth is disabled. |
| `ILUM_CORE_URL` | `http://ilum-core:9888` | ilum-core URL for Bearer token forwarding |
| `ILUM_JWT_PUBLIC_KEY` | *(empty)* | PEM-encoded RSA public key for local JWT validation. If unset, tokens are forwarded to ilum-core. |
| `ILUM_JWT_ISSUER_URI` | `https://ilum.cloud` | Expected JWT `iss` claim for local validation |
| `ILUM_JWT_AUDIENCES` | *(empty)* | Comma-separated list of expected JWT `aud` claims. If empty, audience is not verified. |
| `ILUM_BEARER_CACHE_TTL` | `60` | TTL in seconds for the Bearer token validation cache |
| `ILUM_RELEASE_NAME` | *(auto-detect)* | Helm release name. Falls back to scanning, then `ilum`. |
| `ILUM_NAMESPACE` | `default` | Kubernetes namespace |
| `ILUM_KUBE_CONTEXT` | *(current context)* | Kubernetes context override |
| `ILUM_CHART_REF` | `ilum/ilum` | Helm chart reference for enable/disable/upgrade |

---

## Testing

```bash
# Run all API tests
pytest tests/unit/test_api/ -v

# Single test file
pytest tests/unit/test_api/test_modules.py -v

# With coverage
pytest tests/unit/test_api/ -v --cov=ilum.api --cov-report=term-missing
```

### Test Files

| File | Covers |
|------|--------|
| `test_api/conftest.py` | Shared fixtures (test client, mock manager/store) |
| `test_api/test_status.py` | `/health`, `/release`, `/pods` |
| `test_api/test_modules.py` | `/modules` list, detail, enable, disable |
| `test_api/test_values.py` | `/values` get, diff, update |
| `test_api/test_operations.py` | `/operations` list, get, store lifecycle |
| `test_api/test_auth.py` | Nginx cookie, Bearer JWT (local + forwarding + cache), API key auth |
| `test_api/test_errors.py` | IlumError → HTTP status mapping |
| `test_api/test_startup.py` | Auto-connect / release detection |

All tests use `httpx.AsyncClient` with mocked `ReleaseManager` and `KubeClient` — no real cluster or Helm calls.

---

## Deployment

In production, the API runs as a sidecar container in the `ilum-core` pod, behind the Nginx reverse proxy.

### Nginx Proxy Path

The UI's Nginx config routes `/external/api/` to the API service. Auth is handled at the Nginx layer — it validates the session cookie and sets `X-Ilum-Authenticated: true` before proxying to the API.

### ConfigMap Variables

The API container receives its config via environment variables set in the Helm chart's ConfigMap/values:
- `ILUM_RELEASE_NAME`, `ILUM_NAMESPACE` — injected from the Helm release context
- `ILUM_API_KEY` — set via `ilum-core.apiKey` in `values.yaml`
