# CloudManager API Documentation

CloudManager provides a REST API for multi-tenant network infrastructure management with VPC isolation, external connectivity, and private registry support.

**Base URL:**
- **Local (port-forward):** `https://localhost:8080` &mdash; forward port 8080 of the `cloudmanager-api` service (namespace `cloud-manager`) from the cluster to your machine
- **Public (via Cloudflare tunnel):** `https://api.<your_domain>` &mdash; once the operator's tunnel is provisioned

Every example below uses `https://localhost:8080`; substitute the public URL when you're hitting the tunnel.

## Table of Contents

- [Authentication](#authentication)
- [Tenants](#tenants)
- [Domains](#domains)
- [VPCs (Virtual Private Clouds)](#vpcs-virtual-private-clouds)
- [Subnets](#subnets)
- [Pods (Application Workloads)](#pods-application-workloads)
- [Tunnels (Cloudflare Tunnels)](#tunnels-cloudflare-tunnels)
- [Security Rules](#security-rules)
- [Registry Credentials](#registry-credentials)
- [Topology API (Infrastructure-as-Code)](#topology-api-infrastructure-as-code)
- [Operations Log](#operations-log)
- [Health & Version](#health--version)
- [Error Handling](#error-handling)

---

## Authentication

The API uses token-based authentication with two levels:

### 1. Admin Token
- **Purpose:** Tenant management (create/delete tenants)
- **Location:** `state/.credentials.json` after deployment
- **Header:** `X-Admin-Token: <admin-token>`

### 2. Tenant API Token
- **Purpose:** Resource management within a tenant (VPCs, subnets, pods)
- **Obtained:** Returned when creating a tenant
- **Header:** `X-Auth-Token: <tenant-api-token>`

### Token Rotation

Both admin and tenant tokens can be rotated in place:

- `POST /admin/rotate-token` — admin self-service rotation (auth: `X-Admin-Token`).
- `POST /tenants/{tenant_id}/rotate-token` — admin override (auth: `X-Admin-Token`) or tenant self-service (auth: `X-API-Token` of the same tenant).

Rotation is **single-token**: the old token stops working immediately on success, with no overlap window. The response body contains the new plaintext token, and the GUI auth panel updates in place to show it — copy it from there any time. Logins, logouts, and rotations are written to the ops log.

The cloud-manager GUI resolves the admin token from the database on every request (using the session-cookie username). Rotation is therefore transparent to the GUI — no pod restart, no dropped session. Just remember to also update `state/.credentials.json` on the install host so installer/update scripts keep working.

### Example: Loading Tokens

```bash
# Load admin token
ADMIN_TOKEN=$(python3 -c "import json; print(json.load(open('state/.credentials.json'))['cloudmanager_admin']['api_token'])")

# Store tenant token (from tenant creation response)
TENANT_ID="550e8400-e29b-41d4-a716-446655440000"
TENANT_TOKEN="tenant-api-token-here"
```

---

## Tenants

Tenants provide complete multi-tenant isolation with dedicated namespaces and network resources.

### Create Tenant

Creates a new tenant with isolated namespace and network resources.

**Endpoint:** `POST /tenants`

**Headers:**
```
Content-Type: application/json
X-Admin-Token: <admin-token>
```

**Request Body:**
```json
{
  "name": "team-a",
  "description": "Team A Development Environment",
  "password": "SecurePassword123"
}
```

**Parameters:**
- `name` (string, required): Tenant name (DNS-compliant, lowercase alphanumeric and hyphens)
- `description` (string, optional): Tenant description
- `password` (string, required): Password for tenant GUI login
- `max_pods` (integer, optional): Max logical pods for the tenant
- `max_storage` (integer, optional): Max storage for the tenant (GB)
- `max_vpcs` (integer, optional): Max VPCs for the tenant
- `max_subnets` (integer, optional): Max subnets for the tenant
- `max_cpu` (integer, optional): Max CPU for the tenant (cores)
- `max_ram` (integer, optional): Max RAM for the tenant (GB)

**Defaults (when omitted):**
- `max_pods`: 100
- `max_storage`: 70 GB
- `max_vpcs`: 20
- `max_subnets`: 50
- `max_cpu`, `max_ram`: computed from cluster allocatable resources (fallback 4 cores / 8 GB)

**Response:** `201 Created`
```json
{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "name": "team-a",
  "description": "Team A Development Environment",
  "api_token": "randomly-generated-secure-token",
  "created_at": "2025-01-15T10:30:00Z"
}
```

**Important Notes:**
- The `api_token` is returned only once - store it securely
- Tenant deletion cascades to all VPCs, subnets, pods, tunnels, and network policies

**Example:**
```bash
curl -k -X POST https://localhost:8080/tenants \
  -H "Content-Type: application/json" \
  -H "X-Admin-Token: $ADMIN_TOKEN" \
  -d '{
    "name": "team-a",
    "description": "Team A Environment",
    "password": "SecurePassword123"
  }'
```

### List Tenants

Retrieves all tenants.

**Endpoint:** `GET /tenants`

**Headers:**
```
X-Admin-Token: <admin-token>
```

**Response:** `200 OK`
```json
[
  {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "name": "team-a",
    "description": "Team A Environment",
    "created_at": "2025-01-15T10:30:00Z"
  }
]
```

### Delete Tenant

Deletes a tenant and all associated resources.

**Endpoint:** `DELETE /tenants/{tenant_id}`

**Headers:**
```
X-Admin-Token: <admin-token>
```

**Response:** `204 No Content`

**Warning:** This permanently deletes all VPCs, subnets, pods, tunnels, and network resources.

### Update Tenant Resources

Updates max resource limits for a tenant.

**Endpoint:** `PATCH /tenants/{tenant_id}/resources`

**Headers:**
```
Content-Type: application/json
X-Admin-Token: <admin-token>
```

**Request Body (all fields optional):**
```json
{
  "max_pods": 120,
  "max_storage": 80,
  "max_vpcs": 25,
  "max_subnets": 60,
  "max_cpu": 6,
  "max_ram": 12
}
```

**Response:** `200 OK`
```json
{
  "success": true,
  "message": "Resources updated"
}
```

### Update Default Tenant Resource Limits

Sets the default resource limits applied to new tenants.

**Endpoint:** `PATCH /provider-config/default-resources`

**Headers:**
```
Content-Type: application/json
X-Admin-Token: <admin-token>
```

**Request Body (all fields optional):**
```json
{
  "default_max_pods": 100,
  "default_max_storage": 70,
  "default_max_vpcs": 20,
  "default_max_subnets": 50,
  "default_max_cpu": 4,
  "default_max_ram": 8
}
```

**Response:** `200 OK`
```json
{
  "success": true,
  "message": "Defaults updated"
}
```

---

## Domains

Domains allow admins to assign custom domains to tenants for use with Cloudflare tunnels.

### Add Domain to Tenant

Assigns a custom domain to a tenant.

**Endpoint:** `POST /tenants/{tenant_id}/domains`

**Headers:**
```
Content-Type: application/json
X-Admin-Token: <admin-token>
```

**Request Body:**
```json
{
  "domain": "example.com"
}
```

**Parameters:**
- `domain` (string, required): Domain name to assign (must be configured in Cloudflare)

**Response:** `201 Created`
```json
{
  "id": "domain-uuid-here",
  "domain": "example.com",
  "tenant_id": "550e8400-e29b-41d4-a716-446655440000",
  "created_at": "2025-01-15T10:30:00Z"
}
```

**Example:**
```bash
curl -k -X POST https://localhost:8080/tenants/$TENANT_ID/domains \
  -H "Content-Type: application/json" \
  -H "X-Admin-Token: $ADMIN_TOKEN" \
  -d '{"domain": "example.com"}'
```

### List Tenant Domains

Retrieves all domains assigned to a tenant.

**Endpoint:** `GET /tenants/{tenant_id}/domains`

**Headers:**
```
X-Auth-Token: <tenant-api-token>
```

**Response:** `200 OK`
```json
[
  {
    "id": "domain-uuid-here",
    "domain": "example.com",
    "created_at": "2025-01-15T10:30:00Z"
  }
]
```

### Delete Domain from Tenant

Removes a domain from a tenant.

**Endpoint:** `DELETE /tenants/{tenant_id}/domains/{domain_id}`

**Headers:**
```
X-Admin-Token: <admin-token>
```

**Response:** `200 OK`

**Note:** Existing tunnels using this domain will continue to work until deleted.

---

## VPCs (Virtual Private Clouds)

VPCs provide isolated network environments for your applications.

### Create VPC

Creates a new VPC within a tenant's namespace.

**Endpoint:** `POST /vpcs`

**Headers:**
```
Content-Type: application/json
X-Auth-Token: <tenant-api-token>
```

**Request Body:**
```json
{
  "tenant_id": "550e8400-e29b-41d4-a716-446655440000",
  "token": "<tenant-api-token>",
  "name": "production-vpc",
  "description": "Production environment VPC",
  "enable_internet": true
}
```

**Parameters:**
- `tenant_id` (string, required): Tenant UUID
- `token` (string, required): Tenant API token
- `name` (string, required): VPC name (DNS-compliant, unique per tenant)
- `description` (string, optional): VPC description
- `enable_internet` (boolean, required): Enable external internet access via NAT gateway

**Response:** `201 Created`
```json
{
  "id": "vpc-uuid-here",
  "tenant_id": "550e8400-e29b-41d4-a716-446655440000",
  "name": "production-vpc",
  "description": "Production environment VPC",
  "enable_internet": true,
  "created_at": "2025-01-15T10:30:00Z"
}
```

**Example:**
```bash
curl -k -X POST https://localhost:8080/vpcs \
  -H "Content-Type: application/json" \
  -H "X-Auth-Token: $TENANT_TOKEN" \
  -d '{
    "tenant_id": "'$TENANT_ID'",
    "token": "'$TENANT_TOKEN'",
    "name": "production-vpc",
    "description": "Production environment",
    "enable_internet": true
  }'
```

### Delete VPC

Deletes a VPC and all associated resources (subnets, pods).

**Endpoint:** `DELETE /vpcs/{vpc_id}`

**Headers:**
```
X-Auth-Token: <tenant-api-token>
```

**Response:** `204 No Content`

**Example:**
```bash
curl -k -X DELETE https://localhost:8080/vpcs/$VPC_ID \
  -H "X-Auth-Token: $TENANT_TOKEN"
```

---

## Subnets

Subnets provide IP address ranges within a VPC for pod deployment.

### Create Subnet

Creates a new subnet within a VPC.

**Endpoint:** `POST /subnets`

**Headers:**
```
Content-Type: application/json
X-Auth-Token: <tenant-api-token>
```

**Request Body:**
```json
{
  "tenant_id": "550e8400-e29b-41d4-a716-446655440000",
  "token": "<tenant-api-token>",
  "vpc_id": "vpc-uuid-here",
  "name": "web-tier",
  "cidr": "10.100.1.0/24",
  "gateway_ip": "10.100.1.1"
}
```

**Parameters:**
- `tenant_id` (string, required): Tenant UUID
- `token` (string, required): Tenant API token
- `vpc_id` (string, required): Parent VPC UUID
- `name` (string, required): Subnet name (DNS-compliant, unique per VPC)
- `cidr` (string, required): CIDR block (e.g., "10.100.1.0/24")
- `gateway_ip` (string, required): Gateway IP address within CIDR

**Response:** `201 Created`
```json
{
  "id": "subnet-uuid-here",
  "tenant_id": "550e8400-e29b-41d4-a716-446655440000",
  "vpc_id": "vpc-uuid-here",
  "name": "web-tier",
  "cidr": "10.100.1.0/24",
  "gateway_ip": "10.100.1.1",
  "created_at": "2025-01-15T10:31:00Z"
}
```

**Example:**
```bash
curl -k -X POST https://localhost:8080/subnets \
  -H "Content-Type: application/json" \
  -H "X-Auth-Token: $TENANT_TOKEN" \
  -d '{
    "tenant_id": "'$TENANT_ID'",
    "token": "'$TENANT_TOKEN'",
    "vpc_id": "'$VPC_ID'",
    "name": "web-tier",
    "cidr": "10.100.1.0/24",
    "gateway_ip": "10.100.1.1"
  }'
```

---

## Pods (Application Workloads)

Pods are containerized applications deployed within subnets.

### Create Pod

Deploys a containerized application with optional external access and load balancing.

**Endpoint:** `POST /pods`

**Headers:**
```
Content-Type: application/json
X-Auth-Token: <tenant-api-token>
```

**Request Body:**
```json
{
  "tenant_id": "550e8400-e29b-41d4-a716-446655440000",
  "token": "<tenant-api-token>",
  "vpc_id": "vpc-uuid-here",
  "subnet_id": "subnet-uuid-here",
  "name": "web-app",
  "container_image": "nginx:latest",
  "replica_count": 3,
  "service_port": 80,
  "external_access": true,
  "external_inbound": true,
  "gpu_access": false,
  "persistent": false,
  "persistent_mount_path": "/storage",
  "use_registry_credentials": true,
  "env_vars": {
    "DATABASE_URL": "postgres://app:pw@db:5432/app",
    "LOG_LEVEL": "info"
  }
}
```

**Parameters:**
- `tenant_id` (string, required): Tenant UUID
- `token` (string, required): Tenant API token
- `vpc_id` (string, required): Target VPC UUID
- `subnet_id` (string, required): Target subnet UUID
- `name` (string, required): Pod name (DNS-compliant, unique per tenant)
- `container_image` (string, required): Container image (e.g., "nginx:latest", "myregistry/app:v1.0")
- `replica_count` (integer, required): Number of pod replicas (1-10)
- `service_port` (integer, required): Service port to expose (1-65535)
- `external_access` (boolean, optional, default: false): Enable outbound internet via SNAT
- `external_inbound` (boolean, optional, default: false): Enable inbound internet via EIP/FIP with load balancer
- `gpu_access` (boolean, optional, default: false): Request GPU access for the pod
- `persistent` (boolean, optional, default: false): Enable persistent storage for the pod
- `persistent_mount_path` (string, optional): Mount path inside the container (defaults to `/storage` when persistent)
- `auto_healing` (boolean, optional, default: false): Auto-restart pods on failure (Deployment-backed)
- `auto_scaling` (boolean, optional, default: false): Enable CPU-based autoscaling with HPA (forces auto_healing on)
- `autoscale_max_replicas` (integer, optional, default: 10): Maximum replicas when autoscaling (2-50)
- `autoscale_cpu_target_millicores` (integer, optional, default: 200): Target CPU per pod in millicores (10-10000)
- `use_registry_credentials` (boolean, optional, default: true): Use tenant registry credentials for private images
- `env_vars` (object, optional): Container environment variables as a name → value map. Stored **encrypted at rest** (Fernet) — safe for DB passwords, API keys, etc. Update later via `PATCH /pods/{id}/config`. Example: `{"FOO": "bar", "DATABASE_URL": "..."}`.

**Persistent Storage Notes:**
- Storage is node-local at `/storage/<tenant-id>/<pod-id>` and mounted into the container.
- Persistent pods are pinned to a single worker node; all replicas stay on that node.
- Storage is deleted when the pod is deleted (including cascade deletes).

**External Access Options:**
- `external_access: false, external_inbound: false` - No external access (internal only)
- `external_access: true, external_inbound: false` - Outbound only (SNAT)
- `external_access: false, external_inbound: true` - Inbound only (EIP + FIP + Load Balancer)
- `external_access: true, external_inbound: true` - Full bidirectional access

**Response:** `201 Created`
```json
{
  "id": "pod-uuid-here",
  "tenant_id": "550e8400-e29b-41d4-a716-446655440000",
  "vpc_id": "vpc-uuid-here",
  "subnet_id": "subnet-uuid-here",
  "name": "web-app",
  "container_image": "nginx:latest",
  "replica_count": 3,
  "service_port": 80,
  "external_access": true,
  "external_inbound": true,
  "gpu_access": false,
  "auto_healing": false,
  "auto_scaling": false,
  "eip_address": "203.0.113.100",
  "load_balancer_ip": "10.100.1.200",
  "created_at": "2025-01-15T10:32:00Z"
}
```

**Example:**
```bash
curl -k -X POST https://localhost:8080/pods \
  -H "Content-Type: application/json" \
  -H "X-Auth-Token: $TENANT_TOKEN" \
  -d '{
    "tenant_id": "'$TENANT_ID'",
    "token": "'$TENANT_TOKEN'",
    "vpc_id": "'$VPC_ID'",
    "subnet_id": "'$SUBNET_ID'",
    "name": "web-app",
    "container_image": "nginx:latest",
    "replica_count": 3,
    "service_port": 80,
    "external_access": true,
    "external_inbound": true
  }'
```

### Delete Pod

Deletes a pod and all associated resources (services, EIPs, FIPs, load balancers).

**Endpoint:** `DELETE /pods/{pod_id}`

**Headers:**
```
X-Auth-Token: <tenant-api-token>
```

**Response:** `204 No Content`

**Example:**
```bash
curl -k -X DELETE https://localhost:8080/pods/$POD_ID \
  -H "X-Auth-Token: $TENANT_TOKEN"
```

### Get Pod Logs

Retrieves the last N lines of log output from a pod's primary container.

**Endpoint:** `GET /pods/{pod_id}/logs`

**Headers:**
```
X-Auth-Token: <tenant-api-token>
```

**Query Parameters:**

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `lines` | integer | 100 | Number of log lines to return (1-5000) |

**Response:** `200 OK` — plain text log output

**Example:**
```bash
# Get last 100 lines of logs
curl -k https://localhost:8080/pods/$POD_ID/logs \
  -H "X-Auth-Token: $TENANT_TOKEN"

# Get last 500 lines
curl -k "https://localhost:8080/pods/$POD_ID/logs?lines=500" \
  -H "X-Auth-Token: $TENANT_TOKEN"
```

---

### Restart Pod

Restarts all replicas of a pod.

**Endpoint:** `POST /pods/{pod_id}/restart`

**Headers:**
```
X-Auth-Token: <tenant-api-token>
```

**Response:** `200 OK`
```json
{
  "message": "Pod restart initiated",
  "pod_id": "pod-uuid-here"
}
```

**Example:**
```bash
curl -k -X POST https://localhost:8080/pods/$POD_ID/restart \
  -H "X-Auth-Token: $TENANT_TOKEN"
```

### Stop Pod

Scales the pod's workload to zero replicas. The pod record stays in the database and can be brought back later with `/start`. Network resources (VIP, DNS) are preserved.

**Endpoint:** `POST /pods/{pod_id}/stop`

**Headers:**
```
X-Auth-Token: <tenant-api-token>
```

**Response:** `200 OK`
```json
{ "message": "Pod stopped", "pod_id": "pod-uuid-here" }
```

### Start Pod

Scales a previously-stopped pod back up to its configured replica count.

**Endpoint:** `POST /pods/{pod_id}/start`

**Headers:**
```
X-Auth-Token: <tenant-api-token>
```

**Response:** `200 OK`
```json
{ "message": "Pod started", "pod_id": "pod-uuid-here" }
```

### Scale Replicas

Manually adjust the replica count for a pod. Ignored if autoscaling is enabled (the HPA controls replicas in that case).

**Endpoint:** `POST /pods/{pod_id}/replicas`

**Headers:**
```
Content-Type: application/json
X-Auth-Token: <tenant-api-token>
```

**Request Body:**
```json
{ "replicas": 3 }
```

**Response:** `200 OK`
```json
{ "message": "Replica count updated", "pod_id": "pod-uuid-here", "replicas": 3 }
```

### Reset Replicas

Reset to the pod's original `replica_count`. Useful after manual scaling.

**Endpoint:** `DELETE /pods/{pod_id}/replicas`

**Headers:**
```
X-Auth-Token: <tenant-api-token>
```

**Response:** `200 OK`

### Get Pod Status

Returns per-replica diagnostics: phase, IP, restart count, last termination reason, and any container error messages from the cluster. Use this to drive richer status UIs or to diagnose why a pod isn't ready.

**Endpoint:** `GET /pods/{pod_id}/status`

**Headers:**
```
X-Auth-Token: <tenant-api-token>
```

**Response:** `200 OK`
```json
{
  "pod_id": "pod-uuid-here",
  "status": "ready",
  "replicas": [
    {
      "name": "myapp-0",
      "phase": "Running",
      "ready": true,
      "restart_count": 0,
      "ip": "10.0.1.42",
      "node": "worker-2",
      "reason": null,
      "message": null
    }
  ]
}
```

### List Pod Tunnels

Returns every Cloudflare tunnel attached to a pod.

**Endpoint:** `GET /pods/{pod_id}/tunnels`

**Headers:**
```
X-Auth-Token: <tenant-api-token>
```

**Response:** `200 OK`
```json
{
  "tunnels": [
    {
      "id": "tunnel-uuid",
      "subdomain": "www",
      "domain": "mygames.com",
      "protocol": "http",
      "service_port": 80,
      "status": "active"
    }
  ]
}
```

### Update Pod Configuration

Updates pod runtime configuration (auto-healing, persistent storage, autoscaling). The endpoint stops the running workload, applies configuration changes, and recreates it with new settings. Network resources (VIP, DNS, EIP) are preserved across the update.

**Endpoint:** `PATCH /pods/{pod_id}/config`

**Headers:**
```
Content-Type: application/json
X-Auth-Token: <tenant-api-token>
```

**Request Body (all fields optional):**
```json
{
  "auto_healing": true,
  "persistent": true,
  "persistent_mount_path": "/data",
  "auto_scaling": true,
  "autoscale_min_replicas": 2,
  "autoscale_max_replicas": 8,
  "autoscale_cpu_target_millicores": 300,
  "env_vars": {
    "ADMIN_EMAILS": "ops@acme.example",
    "LOG_LEVEL": "debug"
  }
}
```

**Parameters:**
- `auto_healing` (boolean, optional): Enable/disable auto-healing (Deployment-backed pod recreation on failure)
- `persistent` (boolean, optional): Enable/disable persistent storage (host-path volume pinned to a node)
- `persistent_mount_path` (string, optional): Mount path inside the container for persistent storage (default: `/storage`)
- `auto_scaling` (boolean, optional): Enable/disable CPU-based autoscaling (HPA)
- `autoscale_min_replicas` (integer, optional): Minimum replicas for autoscaling (2-50)
- `autoscale_max_replicas` (integer, optional): Maximum replicas for autoscaling (2-50)
- `autoscale_cpu_target_millicores` (integer, optional): Target average CPU usage per pod in millicores (10-10000)
- `env_vars` (object, optional): **Full replace** of container env vars (not a merge — fields omitted from the dict are removed from the pod). Pass `{}` to clear. Triggers a workload recreate so the new values take effect. Stored encrypted at rest.

**Response:** `200 OK`

Returns the full pod object with updated configuration (same schema as Create Pod response, including `auto_healing`, `auto_scaling`, `persistent`, etc.).

**Edge Cases:**
- Enabling `auto_scaling` automatically enables `auto_healing` (autoscaling requires a Deployment)
- Disabling `auto_healing` while `auto_scaling` is on will also disable autoscaling
- GPU access cannot be changed (hardware dependency) — it is set at creation time only
- There is brief downtime during the workload recreation (old workload is deleted and recreated)

**Example — Enable autoscaling:**
```bash
curl -k -X PATCH https://localhost:8080/pods/$POD_ID/config \
  -H "Content-Type: application/json" \
  -H "X-Auth-Token: $TENANT_TOKEN" \
  -d '{
    "auto_scaling": true,
    "autoscale_max_replicas": 6,
    "autoscale_cpu_target_millicores": 250
  }'
```

**Example — Enable persistent storage:**
```bash
curl -k -X PATCH https://localhost:8080/pods/$POD_ID/config \
  -H "Content-Type: application/json" \
  -H "X-Auth-Token: $TENANT_TOKEN" \
  -d '{
    "persistent": true,
    "persistent_mount_path": "/data"
  }'
```

**Example — Disable all optional features:**
```bash
curl -k -X PATCH https://localhost:8080/pods/$POD_ID/config \
  -H "Content-Type: application/json" \
  -H "X-Auth-Token: $TENANT_TOKEN" \
  -d '{
    "auto_healing": false,
    "persistent": false,
    "auto_scaling": false
  }'
```

**Example — Set / rotate env vars** (full replace; preserve fields you keep, drop anything omitted):
```bash
# To preserve existing values, first GET /pods/{id}, merge with your changes
curl -k -X PATCH https://localhost:8080/pods/$POD_ID/config \
  -H "Content-Type: application/json" \
  -H "X-Auth-Token: $TENANT_TOKEN" \
  -d '{
    "env_vars": {
      "ADMIN_EMAILS": "ops@acme.example",
      "FLASK_SECRET_KEY": "<64-char hex>",
      "LOG_LEVEL": "info"
    }
  }'
```

**Example — Clear all env vars:**
```bash
curl -k -X PATCH https://localhost:8080/pods/$POD_ID/config \
  -H "Content-Type: application/json" \
  -H "X-Auth-Token: $TENANT_TOKEN" \
  -d '{"env_vars": {}}'
```

---

## Tunnels (Cloudflare Tunnels)

Tunnels expose pod services to the internet via Cloudflare's global network with automatic DNS and TLS.

### Create Tunnel

Creates a Cloudflare tunnel for a pod service.

**Endpoint:** `POST /pods/{pod_id}/tunnel`

**Headers:**
```
Content-Type: application/json
X-Auth-Token: <tenant-api-token>
```

**Request Body:**
```json
{
  "tunnel_name": "my-app-tunnel",
  "subdomain": "myapp",
  "protocol_type": "http",
  "service_port": 80,
  "domain": "example.com"
}
```

**Parameters:**
- `tunnel_name` (string, required): Unique tunnel name within tenant
- `subdomain` (string, required): Subdomain to use (creates `subdomain.domain.com`)
- `protocol_type` (string, required): Protocol type - `http`, `https`, `ssh`, `tcp`, `udp`
- `service_port` (integer, optional, default: 80): Target service port
- `domain` (string, optional): Custom domain (must be in tenant's approved domains). If not specified, uses global domain from provider config.

**Response:** `201 Created`
```json
{
  "id": "tunnel-uuid-here",
  "tenant_id": "tenant-uuid",
  "vpc_id": "vpc-uuid",
  "pod_id": "pod-uuid",
  "tunnel_name": "my-app-tunnel",
  "hostname": "myapp.example.com",
  "origin_url": "http://my-app.tenant-namespace.svc.cluster.local:80",
  "protocol_type": "http",
  "status": "active",
  "created_at": "2025-01-15T10:35:00Z"
}
```

**Example:**
```bash
curl -k -X POST https://localhost:8080/pods/$POD_ID/tunnel \
  -H "Content-Type: application/json" \
  -H "X-Auth-Token: $TENANT_TOKEN" \
  -d '{
    "tunnel_name": "web-portal",
    "subdomain": "portal",
    "protocol_type": "http",
    "service_port": 80
  }'
```

### List Tunnels

Retrieves all tunnels for a tenant.

**Endpoint:** `GET /tenants/{tenant_id}/tunnels`

**Headers:**
```
X-Auth-Token: <tenant-api-token>
```

**Response:** `200 OK`
```json
[
  {
    "id": "tunnel-uuid-here",
    "tunnel_name": "my-app-tunnel",
    "hostname": "myapp.example.com",
    "origin_url": "http://my-app.tenant-namespace.svc.cluster.local:80",
    "protocol_type": "http",
    "status": "active",
    "pod_name": "my-app",
    "vpc_name": "production-vpc"
  }
]
```

### Delete Tunnel

Deletes a Cloudflare tunnel.

**Endpoint:** `DELETE /tunnels/{tunnel_id}`

**Headers:**
```
X-Auth-Token: <tenant-api-token>
```

**Response:** `204 No Content`

**Note:** This removes the Cloudflare tunnel, DNS record, and tunnel service pod.

**Example:**
```bash
curl -k -X DELETE https://localhost:8080/tunnels/$TUNNEL_ID \
  -H "X-Auth-Token: $TENANT_TOKEN"
```

### Tunnels in Topology API

Tunnels can also be created inline when deploying pods via the Topology API:

```json
{
  "pods": [
    {
      "name": "web-service",
      "vpc_name": "my-vpc",
      "subnet_name": "my-subnet",
      "container_image": "nginx:latest",
      "service_port": 80,
      "tunnels": [
        {
          "subdomain": "portal",
          "protocol_type": "http",
          "service_port": 80
        },
        {
          "subdomain": "admin",
          "domain": "custom-domain.com",
          "protocol_type": "http",
          "service_port": 8080
        }
      ]
    }
  ]
}
```

---

## Security Rules

Security rules are a per-VPC firewall for your workloads. A rule is: a **direction** (`ingress` or `egress`), an **action** (`allow` or `deny`), a **remote** (an IPv4 CIDR — `0.0.0.0/0` is the internet, i.e. every address — or another of your VPCs), a **protocol** with optional **ports**, a **priority**, a **target** inside the VPC (the whole VPC, one subnet, or specific pods), and an **enabled** flag. Rules are evaluated in priority order and the first match wins: the **lowest** priority number is evaluated first, and at the same priority a `deny` is evaluated before an `allow`. All enum values (`direction`, `action`, `protocol`, `remote_type`, `target_type`) are lowercase.

Three fixed **implied rules** frame everything you write: *always allow the VPC's own core network* (the platform's DNS, tunnels and load balancers, in both directions — no rule can block it, so a deny never takes out your own DNS and exposing a pod never needs a rule for the platform), and beneath your rules *allow all outbound* and *deny all other inbound*. They cannot be edited, reordered, or deleted, and they are returned in the `implied` array of the list response so UIs can show them as a static note. Every new VPC starts with one ordinary, editable rule, **Allow internal traffic** (priority 2000), which lets pods in the VPC reach each other.

All six endpoints authenticate with the tenant token (`X-Auth-Token`) and operate only on the caller's own resources. Naming a VPC or rule that belongs to another tenant returns `404 Not Found` — deliberately not `403`, so a tenant cannot probe whether another tenant's IDs exist. Rules are IPv4-only: an IPv6 CIDR anywhere in a rule is rejected with `422`.

**Enforcement:** every mutation (create, update, delete, reorder) saves the rule first and then applies it to the VPC's network, so a saved rule is normally in force by the time the response returns. Mutation responses report this in two fields: `applied` (boolean) and `apply_error`. The rule is always saved; `applied: false` means the change is saved but not yet in force, `apply_error` says why, and the platform retries automatically every few minutes. Plain GETs change nothing and always report `applied: true`.

**Validation errors** from these endpoints use a structured `422` body that names the offending field:

```json
{
  "detail": {
    "field": "ports",
    "message": "Ports are only valid with protocol tcp or udp — protocol 'icmp' has none. Leave ports blank, or set the protocol to tcp or udp."
  }
}
```

### List Security Rules

Lists one VPC's rules in evaluation order (lowest priority number first), plus the three implied rules.

**Endpoint:** `GET /security-rules?vpc_id={vpc_id}`

**Headers:**
```
X-Auth-Token: <tenant-api-token>
```

**Query Parameters:**
- `vpc_id` (string, required): VPC to list rules for (must belong to the calling tenant)

**Response:** `200 OK`
```json
{
  "vpc_id": "vpc-uuid-here",
  "rules": [
    {
      "id": "rule-uuid-here",
      "tenant_id": "550e8400-e29b-41d4-a716-446655440000",
      "vpc_id": "vpc-uuid-here",
      "name": "Allow internal traffic",
      "description": "Lets pods in this VPC reach each other. Edit or delete it like any other rule.",
      "direction": "ingress",
      "action": "allow",
      "priority": 2000,
      "protocol": "all",
      "ports": null,
      "remote_type": "vpc",
      "remote_cidr": null,
      "remote_vpc_id": "vpc-uuid-here",
      "target_type": "vpc",
      "target_subnet_id": null,
      "target_pod_ids": [],
      "enabled": true,
      "remote_vpc_missing": false,
      "target_missing": false,
      "created_at": "2025-01-15T10:31:00Z",
      "updated_at": "2025-01-15T10:31:00Z"
    }
  ],
  "implied": [
    {
      "key": "implied-allow-infrastructure",
      "name": "Always allow the VPC's core network",
      "description": "Traffic to and from this VPC's own core network — the platform's DNS, tunnels and load balancers — is always allowed, in both directions. No rule can block it, so a deny never takes out your own DNS and exposing a pod never needs a rule for the platform.",
      "direction": "ingress",
      "action": "allow"
    },
    {
      "key": "implied-allow-egress",
      "name": "Allow all outbound",
      "description": "Pods may open outbound connections to any destination that none of your own egress rules denies.",
      "direction": "egress",
      "action": "allow"
    },
    {
      "key": "implied-deny-ingress",
      "name": "Deny all other inbound",
      "description": "Inbound connections are refused unless one of your own ingress rules allows them.",
      "direction": "ingress",
      "action": "deny"
    }
  ],
  "applied": true,
  "apply_error": null
}
```

**Notes:**
- `remote_vpc_missing` / `target_missing` are `true` when the rule references a VPC, subnet, or pod that has since been deleted. Such a rule is **inert** — enforcement skips it rather than widening it to "match anything".
- A rule with `enabled: false` is kept but not enforced.
- This is a pure read: it seeds nothing and changes nothing. A deleted rule stays deleted.

**Example:**
```bash
curl -k -X GET "https://localhost:8080/security-rules?vpc_id=$VPC_ID" \
  -H "X-Auth-Token: $TENANT_TOKEN"
```

### Get Security Rule

Retrieves a single rule by ID.

**Endpoint:** `GET /security-rules/{rule_id}`

**Headers:**
```
X-Auth-Token: <tenant-api-token>
```

**Response:** `200 OK` — one rule object, same shape as the entries in `rules` above.

**Errors:**
- `404 Not Found` — the rule does not exist or belongs to another tenant

**Example:**
```bash
curl -k -X GET https://localhost:8080/security-rules/$RULE_ID \
  -H "X-Auth-Token: $TENANT_TOKEN"
```

### Create Security Rule

Creates a rule on one of the caller's VPCs. Ownership always comes from the token — the body never names a tenant.

**Endpoint:** `POST /security-rules`

**Headers:**
```
Content-Type: application/json
X-Auth-Token: <tenant-api-token>
```

**Request Body:**
```json
{
  "vpc_id": "vpc-uuid-here",
  "name": "allow-https-from-internet",
  "description": "Public HTTPS to the web tier",
  "direction": "ingress",
  "action": "allow",
  "priority": 100,
  "protocol": "tcp",
  "ports": "443",
  "remote_type": "cidr",
  "remote_cidr": "0.0.0.0/0",
  "target_type": "vpc",
  "enabled": true
}
```

**Parameters:**
- `vpc_id` (string, required): VPC this rule belongs to (must be one of your own)
- `name` (string, required): Rule name (1-128 characters)
- `description` (string, optional): Free-text description (max 1024 characters)
- `direction` (string, required): `ingress` or `egress` (lowercase)
- `action` (string, required): `allow` or `deny` (lowercase)
- `priority` (integer, required): 1-2000; the **lowest** number wins
- `protocol` (string, optional, default: `all`): `tcp`, `udp`, `icmp`, or `all`
- `ports` (string, optional): `"443"`, `"8000-9000"`, or a comma list (max 32 entries, ports 1-65535). Omitted/blank = all ports. Only valid with `tcp` or `udp` — a port list on `icmp` or `all` is rejected.
- `remote_type` (string, required): `cidr` or `vpc`
- `remote_cidr` (string, required iff `remote_type` is `cidr`): IPv4 CIDR; `0.0.0.0/0` is the internet; host bits are cleared (`10.0.0.5/24` is stored as `10.0.0.0/24`)
- `remote_vpc_id` (string, required iff `remote_type` is `vpc`): must be one of your own VPCs
- `target_type` (string, optional, default: `vpc`): `vpc`, `subnet`, or `pods`
- `target_subnet_id` (string, required iff `target_type` is `subnet`): must be a subnet of `vpc_id`
- `target_pod_ids` (array of strings, required iff `target_type` is `pods`): max 64 pods, all in `vpc_id`
- `enabled` (boolean, optional, default: `true`): a disabled rule is kept but not enforced

**Response:** `201 Created`
```json
{
  "rule": {
    "id": "rule-uuid-here",
    "tenant_id": "550e8400-e29b-41d4-a716-446655440000",
    "vpc_id": "vpc-uuid-here",
    "name": "allow-https-from-internet",
    "description": "Public HTTPS to the web tier",
    "direction": "ingress",
    "action": "allow",
    "priority": 100,
    "protocol": "tcp",
    "ports": "443",
    "remote_type": "cidr",
    "remote_cidr": "0.0.0.0/0",
    "remote_vpc_id": null,
    "target_type": "vpc",
    "target_subnet_id": null,
    "target_pod_ids": [],
    "enabled": true,
    "remote_vpc_missing": false,
    "target_missing": false,
    "created_at": "2025-01-15T10:40:00Z",
    "updated_at": "2025-01-15T10:40:00Z"
  },
  "applied": true,
  "apply_error": null
}
```

**Errors:**
- `404 Not Found` — `vpc_id` does not exist or belongs to another tenant
- `422 Unprocessable Entity` — a field failed validation; `detail` is `{"field": ..., "message": ...}`. This includes naming a `remote_vpc_id`, `target_subnet_id`, or `target_pod_ids` entry that is not yours or not in this VPC, an IPv6 CIDR, an out-of-range priority, or ports on a portless protocol.
- `409 Conflict` — the VPC already has the maximum of 200 rules

**Example:**
```bash
curl -k -X POST https://localhost:8080/security-rules \
  -H "Content-Type: application/json" \
  -H "X-Auth-Token: $TENANT_TOKEN" \
  -d '{
    "vpc_id": "'$VPC_ID'",
    "name": "allow-https-from-internet",
    "direction": "ingress",
    "action": "allow",
    "priority": 100,
    "protocol": "tcp",
    "ports": "443",
    "remote_type": "cidr",
    "remote_cidr": "0.0.0.0/0",
    "target_type": "vpc"
  }'
```

### Update Security Rule

Partially updates a rule. Only the fields present in the body change. Every field of every rule is editable, `enabled` included — there is no protected rule.

**Endpoint:** `PATCH /security-rules/{rule_id}`

**Headers:**
```
Content-Type: application/json
X-Auth-Token: <tenant-api-token>
```

**Request Body:** any subset of the create parameters except `vpc_id` (a rule cannot move between VPCs). For example:
```json
{
  "priority": 50,
  "enabled": false
}
```

**Parameters:** same names, types, and constraints as [Create Security Rule](#create-security-rule), all optional.

**Response:** `200 OK` — same shape as the create response (`rule`, `applied`, `apply_error`).

**Notes:**
- When you change `remote_type` or `target_type`, supply the matching companion field(s) in the same request (e.g. `remote_type: "cidr"` together with `remote_cidr`). Stale companions from the previous shape are cleared automatically.
- Ports are re-validated whenever `ports` **or** `protocol` changes, so switching a `tcp`/`443` rule to `icmp` is rejected instead of silently keeping a meaningless port list.

**Errors:**
- `404 Not Found` — the rule does not exist or belongs to another tenant
- `422 Unprocessable Entity` — a field failed validation; `detail` is `{"field": ..., "message": ...}`

**Example:**
```bash
curl -k -X PATCH https://localhost:8080/security-rules/$RULE_ID \
  -H "Content-Type: application/json" \
  -H "X-Auth-Token: $TENANT_TOKEN" \
  -d '{"enabled": false}'
```

### Delete Security Rule

Deletes a rule. Any rule — none is protected — and a deleted rule stays deleted; nothing re-seeds it.

**Endpoint:** `DELETE /security-rules/{rule_id}`

**Headers:**
```
X-Auth-Token: <tenant-api-token>
```

**Response:** `200 OK` (not `204`, so the body can report whether the change reached the network)
```json
{
  "rule": null,
  "applied": true,
  "apply_error": null
}
```

**Errors:**
- `404 Not Found` — the rule does not exist or belongs to another tenant

**Example:**
```bash
curl -k -X DELETE https://localhost:8080/security-rules/$RULE_ID \
  -H "X-Auth-Token: $TENANT_TOKEN"
```

### Reorder Security Rules

Rewrites the priorities of every rule in one VPC in a single call. `rule_ids` must list that VPC's rules **exactly once each**, in the order they should be evaluated — a partial list is rejected rather than guessed at. Priorities are re-issued as 10, 20, 30, ..., leaving room to slot a rule between two others later without another full reorder.

**Endpoint:** `POST /security-rules/reorder`

**Headers:**
```
Content-Type: application/json
X-Auth-Token: <tenant-api-token>
```

**Request Body:**
```json
{
  "vpc_id": "vpc-uuid-here",
  "rule_ids": ["rule-uuid-2", "rule-uuid-1", "rule-uuid-3"]
}
```

**Parameters:**
- `vpc_id` (string, required): VPC whose rules are being reordered (must be yours)
- `rule_ids` (array of strings, required): every rule ID in the VPC, exactly once, in the desired evaluation order

**Response:** `200 OK` — same shape as [List Security Rules](#list-security-rules) (`vpc_id`, `rules` with their new priorities, `implied`, `applied`, `apply_error`).

**Errors:**
- `404 Not Found` — `vpc_id` is not yours, or a listed rule is not in this VPC
- `422 Unprocessable Entity` — a rule is listed more than once, or the list is missing some of the VPC's rules

**Example:**
```bash
curl -k -X POST https://localhost:8080/security-rules/reorder \
  -H "Content-Type: application/json" \
  -H "X-Auth-Token: $TENANT_TOKEN" \
  -d '{
    "vpc_id": "'$VPC_ID'",
    "rule_ids": ["'$RULE_ID_2'", "'$RULE_ID_1'", "'$RULE_ID_3'"]
  }'
```

### Example: Allow HTTPS End to End

```bash
#!/bin/bash

# Step 1: Allow inbound HTTPS from the internet (0.0.0.0/0) to the whole VPC
CREATE_RESPONSE=$(curl -sk -X POST https://localhost:8080/security-rules \
  -H "Content-Type: application/json" \
  -H "X-Auth-Token: $TENANT_TOKEN" \
  -d '{
    "vpc_id": "'$VPC_ID'",
    "name": "allow-https-from-internet",
    "description": "Public HTTPS to the web tier",
    "direction": "ingress",
    "action": "allow",
    "priority": 100,
    "protocol": "tcp",
    "ports": "443",
    "remote_type": "cidr",
    "remote_cidr": "0.0.0.0/0",
    "target_type": "vpc"
  }')

RULE_ID=$(echo $CREATE_RESPONSE | jq -r '.rule.id')
echo "Rule ID: $RULE_ID  (applied: $(echo $CREATE_RESPONSE | jq -r '.applied'))"

# Step 2: List the VPC's rules in evaluation order
curl -sk -X GET "https://localhost:8080/security-rules?vpc_id=$VPC_ID" \
  -H "X-Auth-Token: $TENANT_TOKEN" | jq '.rules[] | {name, priority, action}'

# Step 3: Reorder — evaluate the new rule first, the seeded internal rule last
ALL_IDS=$(curl -sk -X GET "https://localhost:8080/security-rules?vpc_id=$VPC_ID" \
  -H "X-Auth-Token: $TENANT_TOKEN" | jq -r '.rules[].id')
INTERNAL_ID=$(echo "$ALL_IDS" | grep -v "$RULE_ID")

curl -sk -X POST https://localhost:8080/security-rules/reorder \
  -H "Content-Type: application/json" \
  -H "X-Auth-Token: $TENANT_TOKEN" \
  -d '{
    "vpc_id": "'$VPC_ID'",
    "rule_ids": ["'$RULE_ID'", "'$INTERNAL_ID'"]
  }' | jq '{applied, rules: [.rules[] | {name, priority}]}'
```

---

## Registry Credentials

Manage private container registry credentials for pulling images.

### Add/Update Registry Credentials

Stores credentials for accessing private registries (Docker Hub, GCR, Harbor, etc.).

**Endpoint:** `POST /tenants/{tenant_id}/registry-credentials`

**Headers:**
```
Content-Type: application/json
X-Auth-Token: <tenant-api-token>
```

**Request Body:**
```json
{
  "name": "dockerhub",
  "server": "https://index.docker.io/v1/",
  "username": "myusername",
  "password": "mytoken"
}
```

**Parameters:**
- `name` (string, required): Unique identifier for this registry (e.g., "dockerhub", "gcr")
- `server` (string, required): Registry server URL
  - Docker Hub: `https://index.docker.io/v1/`
  - GCR: `https://gcr.io`
  - Custom: `https://registry.example.com`
- `username` (string, required): Registry username
- `password` (string, required): Registry password or access token

**Response:** `200 OK`
```json
{
  "message": "Registry credentials added/updated successfully",
  "name": "dockerhub"
}
```

**Example:**
```bash
curl -k -X POST https://localhost:8080/tenants/$TENANT_ID/registry-credentials \
  -H "Content-Type: application/json" \
  -H "X-Auth-Token: $TENANT_TOKEN" \
  -d '{
    "name": "dockerhub",
    "server": "https://index.docker.io/v1/",
    "username": "myusername",
    "password": "dckr_pat_xxxxxxxxxxxxx"
  }'
```

### List Registry Credentials

Retrieves all registry credentials for a tenant (passwords hidden for security).

**Endpoint:** `GET /tenants/{tenant_id}/registry-credentials`

**Headers:**
```
X-Auth-Token: <tenant-api-token>
```

**Response:** `200 OK`
```json
[
  {
    "name": "dockerhub",
    "server": "https://index.docker.io/v1/",
    "username": "myusername"
  },
  {
    "name": "gcr",
    "server": "https://gcr.io",
    "username": "_json_key"
  }
]
```

**Note:** Passwords are never returned in GET responses for security.

**Example:**
```bash
curl -k -X GET https://localhost:8080/tenants/$TENANT_ID/registry-credentials \
  -H "X-Auth-Token: $TENANT_TOKEN"
```

### Delete Registry Credentials

Removes a registry credential from the tenant.

**Endpoint:** `DELETE /tenants/{tenant_id}/registry-credentials/{name}`

**Headers:**
```
X-Auth-Token: <tenant-api-token>
```

**Response:** `200 OK`
```json
{
  "message": "Registry credentials deleted successfully"
}
```

**Example:**
```bash
curl -k -X DELETE https://localhost:8080/tenants/$TENANT_ID/registry-credentials/dockerhub \
  -H "X-Auth-Token: $TENANT_TOKEN"
```

---

## Topology API (Infrastructure-as-Code)

Deploy complete network topologies in a single API call. Ideal for infrastructure-as-code workflows.

### Deploy Topology

Creates VPCs, subnets, and pods from a single JSON definition.

**Endpoint:** `POST /tenants/{tenant_id}/topology`

**Headers:**
```
Content-Type: application/json
```

**Request Body:**
```json
{
  "token": "<tenant-api-token>",
  "registry_credentials": [
    {
      "name": "dockerhub",
      "server": "https://index.docker.io/v1/",
      "username": "myusername",
      "password": "mytoken"
    }
  ],
  "vpcs": [
    {
      "name": "production",
      "description": "Production VPC",
      "enable_internet": true
    }
  ],
  "subnets": [
    {
      "name": "web-tier",
      "vpc_name": "production",
      "cidr": "10.100.1.0/24",
      "gateway_ip": "10.100.1.1"
    },
    {
      "name": "app-tier",
      "vpc_name": "production",
      "cidr": "10.100.2.0/24",
      "gateway_ip": "10.100.2.1"
    }
  ],
  "pods": [
    {
      "name": "web-server",
      "vpc_name": "production",
      "subnet_name": "web-tier",
      "container_image": "nginx:latest",
      "replica_count": 3,
      "service_port": 80,
      "external_access": true,
      "external_inbound": true
    },
    {
      "name": "api-server",
      "vpc_name": "production",
      "subnet_name": "app-tier",
      "container_image": "myregistry/api:v1.0",
      "replica_count": 2,
      "service_port": 8080,
      "external_access": true,
      "external_inbound": false
    }
  ]
}
```

**Parameters:**
- `token` (string, required): Tenant API token
- `registry_credentials` (array, optional): List of registry credentials
- `vpcs` (array, required): At least 1 VPC required
  - `name` (string): VPC name
  - `description` (string, optional): VPC description
  - `enable_internet` (boolean): Enable NAT gateway
- `subnets` (array, required): At least 1 subnet required
  - `name` (string): Subnet name
  - `vpc_name` (string): Reference to VPC name
  - `cidr` (string): CIDR block
  - `gateway_ip` (string): Gateway IP
- `pods` (array, optional): List of pods to deploy
  - `name` (string): Pod name
  - `vpc_name` (string): Reference to VPC name
  - `subnet_name` (string): Reference to subnet name
  - `container_image` (string): Container image
  - `replica_count` (integer): Number of replicas
  - `service_port` (integer): Service port
  - `external_access` (boolean, optional): Enable SNAT
  - `external_inbound` (boolean, optional): Enable EIP/FIP
  - `auto_healing` (boolean, optional): Auto-restart pods

**Response:** `201 Created`
```json
{
  "tenant_id": "550e8400-e29b-41d4-a716-446655440000",
  "vpcs_created": [
    "vpc-uuid-1"
  ],
  "subnets_created": [
    "subnet-uuid-1",
    "subnet-uuid-2"
  ],
  "pods_created": [
    "pod-uuid-1",
    "pod-uuid-2"
  ],
  "message": "Successfully created topology: 1 VPCs, 2 subnets, 2 pods"
}
```

**Important Notes:**
- Topology API is for **initial deployment only**
- Resources with duplicate names will cause conflicts
- To add resources to existing infrastructure, use individual APIs (POST /vpcs, POST /subnets, POST /pods)
- All UUIDs of created resources are returned for reference

**Example:**
```bash
curl -k -X POST https://localhost:8080/tenants/$TENANT_ID/topology \
  -H "Content-Type: application/json" \
  -d @topology.json
```

**Example topology.json:**
```json
{
  "token": "tenant-api-token",
  "vpcs": [
    {"name": "prod", "enable_internet": true}
  ],
  "subnets": [
    {
      "name": "web",
      "vpc_name": "prod",
      "cidr": "10.100.1.0/24",
      "gateway_ip": "10.100.1.1"
    }
  ],
  "pods": [
    {
      "name": "nginx",
      "vpc_name": "prod",
      "subnet_name": "web",
      "container_image": "nginx:latest",
      "replica_count": 2,
      "service_port": 80,
      "external_inbound": true
    }
  ]
}
```

---

## Operations Log

Every mutating operation on the platform — pod create/delete/restart, VPC create/delete, tunnel create/delete, configuration changes — is recorded in an audit log. Read-only API for inspection and dashboards. Server-side filters and cursor-based pagination make it efficient even with thousands of entries.

### List Operations (Global)

Returns ops across all VPCs in the tenant.

**Endpoint:** `GET /ops-log`

**Headers:**
```
X-Auth-Token: <tenant-api-token>
```

**Query parameters (all optional):**
| Param | Type | Notes |
|-------|------|-------|
| `source` | string | `gui`, `api`, or `system` |
| `status` | string | `success` or `failed` |
| `resource_type` | string | `pod`, `vpc`, `subnet`, `tunnel`, `domain`, `registry-credential` |
| `operation` | string | `create`, `delete`, `update`, `start`, `stop`, `restart` |
| `limit` | int | Default 50, max 200 |
| `cursor` | string | Opaque cursor from a previous response's `next_cursor` |

**Response:** `200 OK`
```json
{
  "items": [
    {
      "id": "op-uuid",
      "timestamp": "2026-05-15T09:31:22Z",
      "source": "api",
      "actor": "tenant-abc",
      "operation": "create",
      "resource_type": "pod",
      "resource_id": "pod-uuid",
      "vpc_id": "vpc-uuid",
      "status": "success",
      "error_message": null,
      "details": { "name": "myapp" }
    }
  ],
  "next_cursor": "eyJ0cyI6IjIwMjYtMDUtMTVUMDk6MzE6MjJaIn0="
}
```

**Example:**
```bash
# All failed pod operations in this tenant
curl -k -G "https://localhost:8080/ops-log" \
  -H "X-Auth-Token: $TENANT_TOKEN" \
  --data-urlencode "resource_type=pod" \
  --data-urlencode "status=failed"
```

### List Operations (VPC-scoped)

Same as above but limited to operations inside one VPC. Useful for per-VPC dashboards.

**Endpoint:** `GET /vpcs/{vpc_id}/ops-log`

Query params and response shape identical to `/ops-log`.

---

## Health & Version

### Health Check

Check API health status.

**Endpoint:** `GET /healthz`

**Response:** `200 OK`
```json
{
  "status": "healthy"
}
```

### Version

Get API version information.

**Endpoint:** `GET /version`

**Response:** `200 OK`
```json
{
  "version": "1.3.87",
  "build_date": "2025-01-15"
}
```

---

## Error Handling

The API uses standard HTTP status codes and returns errors in JSON format.

### HTTP Status Codes

- `200 OK` - Request succeeded
- `201 Created` - Resource created successfully
- `204 No Content` - Resource deleted successfully
- `400 Bad Request` - Invalid request parameters
- `401 Unauthorized` - Missing or invalid authentication token
- `403 Forbidden` - Insufficient permissions
- `404 Not Found` - Resource not found
- `409 Conflict` - Resource already exists (e.g., duplicate name)
- `422 Unprocessable Entity` - Request body failed validation
- `500 Internal Server Error` - Server error

### Error Response Format

```json
{
  "detail": "Error message describing what went wrong"
}
```

### Common Errors

**Missing Authentication:**
```json
{
  "detail": "Admin token required (X-Admin-Token header)"
}
```

**Invalid Token:**
```json
{
  "detail": "Invalid token"
}
```

**Resource Conflict:**
```json
{
  "detail": "VPC with name 'production-vpc' already exists for this tenant."
}
```

**Validation Error:**
```json
{
  "detail": [
    {
      "loc": ["body", "cidr"],
      "msg": "Invalid CIDR format",
      "type": "value_error"
    }
  ]
}
```

---

## Best Practices

### 1. Token Security
- **Never commit tokens to version control**
- Store admin token securely in `state/.credentials.json`
- Store tenant tokens in secure credential management systems
- Rotate tokens regularly

### 2. Resource Naming
- Use DNS-compliant names (lowercase, alphanumeric, hyphens)
- Use descriptive names for easier management
- Avoid generic names like "test", "app", "pod1"

### 3. Network Planning
- Plan your CIDR blocks to avoid overlaps
- Use RFC1918 private address space (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16)
- Reserve address space for future growth
- Document your IP addressing scheme

### 4. External Access
- Only enable `external_inbound` for services that need public access
- Use `external_access` (SNAT) for outbound-only requirements
- Consider security implications of public exposure
- Use load balancers (`external_inbound: true`) for high availability
- Pair public exposure with security rules — allow only the ports the service actually serves

### 5. Registry Credentials
- Use access tokens instead of passwords when possible
- Update credentials immediately if compromised
- Test credentials before deploying production workloads
- Use separate credentials for different environments

### 6. Topology API
- Store topology definitions in version control
- Test topologies in development before production
- Use descriptive names for all resources
- Document network architecture decisions

---

## Examples

### Complete Workflow Example

```bash
#!/bin/bash

# Step 1: Load admin token
ADMIN_TOKEN=$(python3 -c "import json; print(json.load(open('state/.credentials.json'))['cloudmanager_admin']['api_token'])")

# Step 2: Create tenant
TENANT_RESPONSE=$(curl -s -X POST https://localhost:8080/tenants \
  -H "Content-Type: application/json" \
  -H "X-Admin-Token: $ADMIN_TOKEN" \
  -d '{
    "name": "production",
    "description": "Production Environment",
    "password": "SecurePassword123"
  }')

TENANT_ID=$(echo $TENANT_RESPONSE | jq -r '.id')
TENANT_TOKEN=$(echo $TENANT_RESPONSE | jq -r '.api_token')

echo "Tenant ID: $TENANT_ID"
echo "Tenant Token: $TENANT_TOKEN"

# Step 3: Add registry credentials
curl -k -X POST https://localhost:8080/tenants/$TENANT_ID/registry-credentials \
  -H "Content-Type: application/json" \
  -H "X-Auth-Token: $TENANT_TOKEN" \
  -d '{
    "name": "dockerhub",
    "server": "https://index.docker.io/v1/",
    "username": "myusername",
    "password": "dckr_pat_xxxxx"
  }'

# Step 4: Deploy topology
curl -k -X POST https://localhost:8080/tenants/$TENANT_ID/topology \
  -H "Content-Type: application/json" \
  -d '{
    "token": "'$TENANT_TOKEN'",
    "vpcs": [
      {"name": "prod-vpc", "enable_internet": true}
    ],
    "subnets": [
      {
        "name": "web-subnet",
        "vpc_name": "prod-vpc",
        "cidr": "10.100.1.0/24",
        "gateway_ip": "10.100.1.1"
      }
    ],
    "pods": [
      {
        "name": "web-app",
        "vpc_name": "prod-vpc",
        "subnet_name": "web-subnet",
        "container_image": "myusername/webapp:latest",
        "replica_count": 3,
        "service_port": 80,
        "external_inbound": true
      }
    ]
  }'

echo "Deployment complete!"
```

---

## Web GUI Interfaces

In addition to the REST API, CloudManager provides web-based interfaces for managing infrastructure:

### CloudManager GUI (Admin Portal)

**URL:** `https://localhost:8443`

Admin portal for platform management:
- Tenant management (create, view, delete)
- Domain assignment to tenants
- Infrastructure overview across all tenants
- Interactive topology visualization
- Pod console access (web terminal)
- Tunnel management

### CloudUser GUI (Tenant Portal)

**URL:** `http://localhost:8444`

Self-service portal for tenants:
- VPC and subnet management
- Pod deployment with replicas
- Registry credentials configuration
- Tunnel creation for internet exposure
- Console access to pods
- Real-time status updates

**Note:** Both GUIs feature automatic real-time updates - changes made in one GUI are reflected in others without page refresh.

---

## Support

For issues, questions, or feature requests:
- Check the [README](README.md) for setup instructions
- Review test scenarios in `/scenarios` directory
- Report issues on the project repository

---

**API Version:** 1.6.0
**Last Updated:** March 2026

## Rate Limiting

CloudManager implements per-tenant rate limiting to ensure fair API usage. Each tenant has configurable rate limits:

- **Default**: 60 requests per minute
- **Burst**: 100 requests per second
- **Response**: HTTP 429 when limit exceeded

### Rate Limit Headers

Every API response includes rate limit information:

```
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 45
X-RateLimit-Reset: 1704123456
```

### Example Rate Limit Response

```bash
curl -k -X POST https://localhost:8080/vpcs \
  -H "Content-Type: application/json" \
  -H "X-Auth-Token: <tenant-token>" \
  -d '{"tenant_id":"xxx","name":"my-vpc"}'

# Response when rate limit exceeded:
# HTTP/1.1 429 Too Many Requests
# Retry-After: 45
# X-RateLimit-Limit: 60
# X-RateLimit-Remaining: 0
#
# {
#   "detail": "Rate limit exceeded. Maximum 60 requests per minute allowed. Try again in 45 seconds."
# }
```

For detailed information, see [RATE_LIMITING.md](RATE_LIMITING.md).
