> ## Documentation index
> Fetch the complete documentation index at: https://docs.katakate.org/llms.txt
> Ask it with GET https://docs.katakate.org/ask?q=<keywords or question>. Short keywords rank best.
> Use these to discover all available pages before exploring further.

# Tenants, usage, node dedication, install

> Tenant CRUD with quotas and defaults, Prometheus showback, dedicating nodes, and remote node install

A `Tenant` (`tenants.k7.katakate.org/v1alpha1`, cluster-scoped) claims namespaces, dedicated nodes, a default sandbox size, quotas and per-VM limits. Concepts and semantics: [Tenants, roles and OIDC](/k7/guides/tenants-and-roles).

## Tenants

| Method | Path | Role |
|---|---|---|
| GET | `/api/v1/tenants` | `operator`, `admin` |
| GET | `/api/v1/tenants/{name}` | `operator`, `admin` |
| POST | `/api/v1/tenants` | `admin` |
| PATCH | `/api/v1/tenants/{name}` | `admin` |
| DELETE | `/api/v1/tenants/{name}[?cascade=true]` | `admin` |

<RequestExample>
```bash cURL
curl --cacert ./k7-ca.crt -X POST "$BASE/api/v1/tenants" \
  -H "X-API-Key: $K7_ADMIN_KEY" -H "Content-Type: application/json" \
  -d '{
    "name": "acme",
    "spec": {
      "namespaces": ["acme-dev", "acme-prod"],
      "nodes": ["k7-node-01"],
      "defaultSize": {"cpu": "1", "memory": "2Gi", "disk": "10Gi"},
      "quotas": {"sandboxes": 20, "vcpu": "16", "memory": "32Gi", "snapshotStorage": "200Gi"},
      "limits": {
        "maxSandboxSize": {"cpu": "8", "memory": "32Gi"},
        "network": {"egressBps": "100M", "ingressBps": "100M"}
      }
    }
  }'
```
</RequestExample>

<ResponseExample>
```json GET /api/v1/tenants/acme
{
  "data": {
    "apiVersion": "k7.katakate.org/v1alpha1",
    "kind": "Tenant",
    "metadata": { "name": "acme" },
    "spec": { "namespaces": ["acme-dev", "acme-prod"], "nodes": ["k7-node-01"], "quotas": { "sandboxes": 20, "vcpu": "16", "memory": "32Gi", "snapshotStorage": "200Gi" } },
    "status": {
      "usage": { "sandboxes": "3", "vcpu": "3", "memory": "6Gi", "disk": "30Gi", "snapshotStorage": "0" },
      "observedGeneration": 2
    }
  }
}
```
</ResponseExample>

- `spec.namespaces` (required, ≥ 1): created if missing and labelled `k7.katakate.org/tenant`. A namespace another tenant already claims is `400`.
- `spec.nodes`: the tenant's sandboxes land only on these nodes (selector + toleration). Dedicate them with the node routes below so other tenants stay off.
- `spec.defaultSize` (`cpu`, `memory`, `disk`): filled into creates without limits. Production mode requires CPU and memory on every sandbox, so set it or make callers pass `limits`.
- `spec.quotas` (`sandboxes`, `vcpu`, `memory`, `disk`, `snapshotStorage`): checked at the Deployment and VolumeSnapshot creates under a per-tenant Lease, so concurrent creates on several API replicas cannot overshoot. Usage is **reserved** (a paused sandbox keeps its vCPU/memory/disk). Over-quota: `403 QuotaExceeded`. `status.usage` is rewritten every 60 s by the leader replica.
- `spec.limits.maxSandboxSize`: a request above it is `400`. `limits.network` (`egressBps`/`ingressBps`, bits/s) is enforced on kfd and kql; `limits.diskIO` is accepted by the CRD but no backend enforces it yet — a tenant that sets it (or `network` / a `disk` quota on k7d / k7d-fc) gets `400 LimitNotEnforceable` on create rather than running unlimited.
- `PATCH` replaces whole sections; `null` removes one. `DELETE` refuses a tenant that still has sandboxes unless `cascade=true`.

For a single-namespace tenant K7 also writes a Kubernetes `ResourceQuota` (`k7-tenant-quota`) as a backstop against `kubectl apply` bypassing the API; multi-namespace tenants have none.

## Usage (showback)

Endpoint: `GET /api/v1/usage?from=&to=&tenant=&group_by=`

<RequestExample>
```bash cURL
curl --cacert ./k7-ca.crt -H "X-API-Key: $K7_API_KEY" \
  "$BASE/api/v1/usage?from=1760659200&to=1760745600&tenant=acme&group_by=namespace,state"
```
</RequestExample>

<ParamField query="from" type="string" required>Unix seconds or RFC 3339 with offset</ParamField>
<ParamField query="to" type="string" required>Same formats</ParamField>
<ParamField query="tenant" type="string">A tenant key must name its own; omit (operator/admin) for all tenants</ParamField>
<ParamField query="group_by" type="string">Subset of `tenant,namespace,backend,state`</ParamField>

Returns `rows` of `sandbox_seconds`, `vcpu_seconds`, `memory_gib_seconds`, `egress_bytes`, `disk_gib_seconds` and `snapshot_gib_seconds` per group, read from the Prometheus at `metering.prometheusUrl` (`k7 install --monitoring` or `--metering-prometheus-url`). Without one the route answers `400` naming the key; an unreachable Prometheus is `502 PrometheusUnavailable`. K7 keeps no usage database — the CloudEvents usage stream is the record for billing. See [Observability and metering](/k7/guides/observability-metering).

## Node dedication

| Method | Path | Body | Role |
|---|---|---|---|
| POST | `/api/v1/nodes/{name}/dedicate` | `{"tenant": "acme"}` | `operator`, `admin` |
| POST | `/api/v1/nodes/{name}/undedicate` | — | `operator`, `admin` |

Dedicating writes the `k7.katakate.org/tenant` label **and** a `NoSchedule` taint on the node and adds it to `Tenant.spec.nodes`; undedicating reverses all three. `{name}` is the Kubernetes node name (`k7 nodes list`); an unknown node or tenant is `404`.

## Remote install

| Method | Path | Role |
|---|---|---|
| POST | `/api/v1/install` | `admin` |
| GET | `/api/v1/install/{operation}` | `admin` |

Off by default (`404`). `k7 install --api-remote-install` (development mode only) deploys the `k7-node-installer` DaemonSet in `k7-system`, configured from the first master and pinned to the digest in `images.lock.yaml`. The API never runs Ansible: `POST` takes exactly `{"nodes": ["name", …]}`, labels those nodes into the installer pool and returns `202` with an operation id; `GET` reports each node `pending` / `running` / `succeeded` / `failed` with the installer's own failure line.

Any other body key (`playbook`, `image`, `inventory`, `config`) is `400`; an unknown node `404`; a K3s server node without `--node-installer-allow-server-restart` `409`; a DaemonSet whose image drifted from the pin `500`. SDK: `Client.install(nodes)` / `install_status(operation)`.
