|
@@ -0,0 +1,223 @@
|
|
|
|
|
+# Inventory API
|
|
|
|
|
+
|
|
|
|
|
+RackPeek exposes a small HTTP API for upserting inventory from scripts and
|
|
|
|
|
+external systems — useful for automatic ingestion from Ansible, Terraform,
|
|
|
|
|
+discovery scanners, or any other tool that can produce a YAML or JSON
|
|
|
|
|
+description of your infrastructure.
|
|
|
|
|
+
|
|
|
|
|
+The API is served by the same process as the Web UI, on the same port.
|
|
|
|
|
+
|
|
|
|
|
+## What you need
|
|
|
|
|
+
|
|
|
|
|
+1. **The server URL** — wherever RackPeek is running, e.g.
|
|
|
|
|
+ `http://localhost:8080`.
|
|
|
|
|
+2. **An API key** — a shared secret you choose, set via the `RPK_API_KEY`
|
|
|
|
|
+ environment variable on the server. Requests without a matching key
|
|
|
|
|
+ are rejected.
|
|
|
|
|
+
|
|
|
|
|
+## Configuring the API key
|
|
|
|
|
+
|
|
|
|
|
+Set `RPK_API_KEY` on the RackPeek container or process. Until you set it,
|
|
|
|
|
+the inventory endpoint returns `503 Service Unavailable` for every request.
|
|
|
|
|
+
|
|
|
|
|
+| Variable | Required | Description |
|
|
|
|
|
+|---|---|---|
|
|
|
|
|
+| `RPK_API_KEY` | Yes (to use the API) | Shared secret. Clients must send this in the `X-Api-Key` header. |
|
|
|
|
|
+
|
|
|
|
|
+Example with Docker Compose:
|
|
|
|
|
+
|
|
|
|
|
+```yaml
|
|
|
|
|
+version: "3.9"
|
|
|
|
|
+
|
|
|
|
|
+services:
|
|
|
|
|
+ rackpeek:
|
|
|
|
|
+ image: aptacode/rackpeek:latest
|
|
|
|
|
+ container_name: rackpeek
|
|
|
|
|
+ ports:
|
|
|
|
|
+ - "8080:8080"
|
|
|
|
|
+ volumes:
|
|
|
|
|
+ - rackpeek-config:/app/config
|
|
|
|
|
+ environment:
|
|
|
|
|
+ - RPK_API_KEY=change-me-to-a-long-random-string
|
|
|
|
|
+ restart: unless-stopped
|
|
|
|
|
+
|
|
|
|
|
+volumes:
|
|
|
|
|
+ rackpeek-config:
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+Pick a long random value (e.g. `openssl rand -hex 32`). The server compares
|
|
|
|
|
+keys in constant time, so timing-based guessing is not viable, but a weak
|
|
|
|
|
+key still loses to brute force — treat it like a password.
|
|
|
|
|
+
|
|
|
|
|
+## Endpoints
|
|
|
|
|
+
|
|
|
|
|
+### `GET /health`
|
|
|
|
|
+
|
|
|
|
|
+Liveness check. No authentication. Returns `200 OK` with body `rackpeek` as
|
|
|
|
|
+`text/plain`. Used by the Docker healthcheck.
|
|
|
|
|
+
|
|
|
|
|
+```bash
|
|
|
|
|
+curl http://localhost:8080/health
|
|
|
|
|
+# rackpeek
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### `POST /api/inventory`
|
|
|
|
|
+
|
|
|
|
|
+Upsert resources and connections. Requires the `X-Api-Key` header.
|
|
|
|
|
+
|
|
|
|
|
+#### Request body
|
|
|
|
|
+
|
|
|
|
|
+| Field | Type | Required | Description |
|
|
|
|
|
+|---|---|---|---|
|
|
|
|
|
+| `yaml` | string | one of `yaml` or `json` | Raw YAML payload, same schema as `config.yaml`. |
|
|
|
|
|
+| `json` | object | one of `yaml` or `json` | Structured JSON payload (see shape below). |
|
|
|
|
|
+| `mode` | `"Merge"` \| `"Replace"` | No (default `Merge`) | How incoming resources combine with existing ones. |
|
|
|
|
|
+| `dryRun` | bool | No (default `false`) | Compute the diff and return it without writing to disk. |
|
|
|
|
|
+
|
|
|
|
|
+Exactly one of `yaml` or `json` must be supplied — sending both is rejected.
|
|
|
|
|
+
|
|
|
|
|
+#### Merge semantics
|
|
|
|
|
+
|
|
|
|
|
+For resources whose `name` doesn't already exist, the resource is added.
|
|
|
|
|
+For resources whose `name` already exists:
|
|
|
|
|
+
|
|
|
|
|
+- **`Merge`** — fields present in the incoming payload overwrite the existing
|
|
|
|
|
+ values; fields that are omitted, `null`, empty lists, or empty maps in the
|
|
|
|
|
+ incoming payload are left untouched. Use this for partial updates.
|
|
|
|
|
+- **`Replace`** — the incoming resource fully replaces the existing one,
|
|
|
|
|
+ even for omitted fields. Use this when the incoming payload is the
|
|
|
|
|
+ authoritative full picture.
|
|
|
|
|
+
|
|
|
|
|
+If an incoming resource has a different `kind` than the existing one with
|
|
|
|
|
+the same name (e.g. server → switch), the existing resource is fully
|
|
|
|
|
+replaced regardless of mode.
|
|
|
|
|
+
|
|
|
|
|
+#### JSON payload shape
|
|
|
|
|
+
|
|
|
|
|
+The `json` field accepts the same shape as `config.yaml` — a root object
|
|
|
|
|
+with `version`, `resources`, and optional `connections`. The canonical
|
|
|
|
|
+schemas live in [`schemas/`](https://github.com/Timmoth/RackPeek/tree/main/schemas)
|
|
|
|
|
+in the repository.
|
|
|
|
|
+
|
|
|
|
|
+```json
|
|
|
|
|
+{
|
|
|
|
|
+ "version": 3,
|
|
|
|
|
+ "resources": [
|
|
|
|
|
+ {
|
|
|
|
|
+ "kind": "server",
|
|
|
|
|
+ "name": "web-01",
|
|
|
|
|
+ "tags": ["homelab", "prod"],
|
|
|
|
|
+ "labels": { "env": "production" },
|
|
|
|
|
+ "ipmi": true,
|
|
|
|
|
+ "ram": { "size": 64 }
|
|
|
|
|
+ }
|
|
|
|
|
+ ],
|
|
|
|
|
+ "connections": []
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+#### Response
|
|
|
|
|
+
|
|
|
|
|
+`200 OK` with a diff summary:
|
|
|
|
|
+
|
|
|
|
|
+| Field | Description |
|
|
|
|
|
+|---|---|
|
|
|
|
|
+| `added` | Names of resources that were newly created. |
|
|
|
|
|
+| `updated` | Names of existing resources whose fields changed under `Merge` mode. |
|
|
|
|
|
+| `replaced` | Names of resources fully overwritten (either `Replace` mode, or a kind mismatch). |
|
|
|
|
|
+| `oldYaml` | YAML snapshot of each updated/replaced resource **before** the change, keyed by name. |
|
|
|
|
|
+| `newYaml` | YAML snapshot of each incoming resource **after** the change, keyed by name. |
|
|
|
|
|
+
|
|
|
|
|
+`oldYaml` is omitted for newly-added resources (nothing existed to snapshot).
|
|
|
|
|
+
|
|
|
|
|
+#### Error responses
|
|
|
|
|
+
|
|
|
|
|
+| Status | When |
|
|
|
|
|
+|---|---|
|
|
|
|
|
+| `400 Bad Request` | Validation failure — neither `yaml` nor `json` supplied, both supplied, malformed payload, duplicate resource names, etc. Body: `{ "error": "..." }`. |
|
|
|
|
|
+| `401 Unauthorized` | `X-Api-Key` header missing or doesn't match `RPK_API_KEY`. |
|
|
|
|
|
+| `503 Service Unavailable` | `RPK_API_KEY` is not configured on the server. |
|
|
|
|
|
+
|
|
|
|
|
+## Examples
|
|
|
|
|
+
|
|
|
|
|
+### Upsert from a YAML file with curl
|
|
|
|
|
+
|
|
|
|
|
+```bash
|
|
|
|
|
+curl -X POST http://localhost:8080/api/inventory \
|
|
|
|
|
+ -H "X-Api-Key: $RPK_API_KEY" \
|
|
|
|
|
+ -H "Content-Type: application/json" \
|
|
|
|
|
+ -d "$(jq -Rs --arg mode Merge '{yaml: ., mode: $mode}' < inventory.yaml)"
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### Upsert a single resource as JSON
|
|
|
|
|
+
|
|
|
|
|
+```bash
|
|
|
|
|
+curl -X POST http://localhost:8080/api/inventory \
|
|
|
|
|
+ -H "X-Api-Key: $RPK_API_KEY" \
|
|
|
|
|
+ -H "Content-Type: application/json" \
|
|
|
|
|
+ -d '{
|
|
|
|
|
+ "mode": "Merge",
|
|
|
|
|
+ "json": {
|
|
|
|
|
+ "version": 3,
|
|
|
|
|
+ "resources": [
|
|
|
|
|
+ {
|
|
|
|
|
+ "kind": "server",
|
|
|
|
|
+ "name": "web-01",
|
|
|
|
|
+ "tags": ["homelab"],
|
|
|
|
|
+ "ram": { "size": 64 }
|
|
|
|
|
+ }
|
|
|
|
|
+ ]
|
|
|
|
|
+ }
|
|
|
|
|
+ }'
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### Preview a change with `dryRun`
|
|
|
|
|
+
|
|
|
|
|
+```bash
|
|
|
|
|
+curl -X POST http://localhost:8080/api/inventory \
|
|
|
|
|
+ -H "X-Api-Key: $RPK_API_KEY" \
|
|
|
|
|
+ -H "Content-Type: application/json" \
|
|
|
|
|
+ -d '{
|
|
|
|
|
+ "dryRun": true,
|
|
|
|
|
+ "mode": "Replace",
|
|
|
|
|
+ "json": {
|
|
|
|
|
+ "version": 3,
|
|
|
|
|
+ "resources": [
|
|
|
|
|
+ { "kind": "server", "name": "web-01", "ram": { "size": 128 } }
|
|
|
|
|
+ ]
|
|
|
|
|
+ }
|
|
|
|
|
+ }'
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+The response shows what would change without modifying `config.yaml`.
|
|
|
|
|
+
|
|
|
|
|
+### Bash helper
|
|
|
|
|
+
|
|
|
|
|
+```bash
|
|
|
|
|
+rpk_ingest() {
|
|
|
|
|
+ local file="$1"
|
|
|
|
|
+ curl -fsS -X POST "${RPK_URL:-http://localhost:8080}/api/inventory" \
|
|
|
|
|
+ -H "X-Api-Key: $RPK_API_KEY" \
|
|
|
|
|
+ -H "Content-Type: application/json" \
|
|
|
|
|
+ -d "$(jq -Rs '{yaml: ., mode: "Merge"}' < "$file")"
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+rpk_ingest inventory.yaml
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+## Tips
|
|
|
|
|
+
|
|
|
|
|
+- Run with `dryRun: true` first when wiring up a new pipeline — the response
|
|
|
|
|
+ tells you exactly what would change.
|
|
|
|
|
+- Prefer `Merge` when your script only knows about a subset of fields (e.g.
|
|
|
|
|
+ a discovery scanner that only sees CPU/RAM but doesn't know about
|
|
|
|
|
+ labels/tags). Use `Replace` when the script owns the full resource.
|
|
|
|
|
+- The `tags` array (a list) is replaced wholesale by any non-empty incoming
|
|
|
|
|
+ value, even in `Merge` mode — if you want to add a single tag without
|
|
|
|
|
+ overwriting the others, fetch the resource first or use the CLI
|
|
|
|
|
+ (`rpk <kind> tag add ...`).
|
|
|
|
|
+- The `labels` map (a dictionary) is merged key-wise in `Merge` mode —
|
|
|
|
|
+ existing keys absent from the incoming payload are preserved; keys
|
|
|
|
|
+ present in the incoming payload overwrite the existing value.
|
|
|
|
|
+- The same YAML you commit to Git via the Git integration is what the API
|
|
|
|
|
+ reads and writes — there is no separate API store.
|