# 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": []
}
```

The `kind` discriminator is case-sensitive. Use the exact value for each
resource type: `Server`, `Switch`, `Router`, `Firewall`, `AccessPoint`,
`Ups`, `Desktop`, `Laptop`, `Service`, `System`.

#### 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.
