photoncloud-monorepo/specifications/rest-api-patterns.md
centra d2149b6249 fix(lightningstor): Fix SigV4 canonicalization for AWS S3 auth
- Replace form_urlencoded with RFC 3986 compliant URI encoding
- Implement aws_uri_encode() matching AWS SigV4 spec exactly
- Unreserved chars (A-Z,a-z,0-9,-,_,.,~) not encoded
- All other chars percent-encoded with uppercase hex
- Preserve slashes in paths, encode in query params
- Normalize empty paths to '/' per AWS spec
- Fix test expectations (body hash, HMAC values)
- Add comprehensive SigV4 signature determinism test

This fixes the canonicalization mismatch that caused signature
validation failures in T047. Auth can now be enabled for production.

Refs: T058.S1
2025-12-12 06:23:46 +09:00

363 lines
9 KiB
Markdown

# PhotonCloud REST API Patterns
**Status:** Draft (T050.S1)
**Created:** 2025-12-12
**Author:** PeerA
## Overview
This document defines consistent REST API patterns for all PhotonCloud services.
Goal: curl/シェルスクリプト/組み込み環境で簡単に使える HTTP API.
## URL Structure
```
{scheme}://{host}:{port}/api/v1/{resource}[/{id}][/{action}]
```
### Examples
```
GET /api/v1/kv/mykey # Get key
PUT /api/v1/kv/mykey # Put key
DELETE /api/v1/kv/mykey # Delete key
GET /api/v1/vms # List VMs
POST /api/v1/vms # Create VM
GET /api/v1/vms/vm-123 # Get VM
DELETE /api/v1/vms/vm-123 # Delete VM
POST /api/v1/vms/vm-123/start # Start VM (action)
POST /api/v1/vms/vm-123/stop # Stop VM (action)
```
## HTTP Methods
| Method | Usage | Idempotent |
|--------|-------|------------|
| GET | Read resource(s) | Yes |
| POST | Create resource or execute action | No |
| PUT | Create or replace resource | Yes |
| DELETE | Delete resource | Yes |
| PATCH | Partial update (optional) | No |
## Request Format
### Content-Type
```
Content-Type: application/json
```
### Authentication
```
Authorization: Bearer <jwt-token>
```
Token obtained from IAM:
```bash
# Get token
curl -X POST http://iam:8081/api/v1/auth/token \
-H "Content-Type: application/json" \
-d '{"username": "admin", "password": "secret"}'
# Response
{"token": "eyJ..."}
# Use token
curl http://chainfire:8082/api/v1/kv/mykey \
-H "Authorization: Bearer eyJ..."
```
### Request Body (POST/PUT)
```json
{
"field1": "value1",
"field2": 123
}
```
## Response Format
### Success Response
```json
{
"data": {
// Resource data
},
"meta": {
"request_id": "req-abc123",
"timestamp": "2025-12-12T01:40:00Z"
}
}
```
### List Response
```json
{
"data": [
{ "id": "item-1", ... },
{ "id": "item-2", ... }
],
"meta": {
"total": 42,
"limit": 20,
"offset": 0,
"request_id": "req-abc123"
}
}
```
### Error Response
```json
{
"error": {
"code": "NOT_FOUND",
"message": "Resource not found",
"details": {
"resource": "vm",
"id": "vm-123"
}
},
"meta": {
"request_id": "req-abc123",
"timestamp": "2025-12-12T01:40:00Z"
}
}
```
### Error Codes
| HTTP Status | Error Code | Description |
|-------------|------------|-------------|
| 400 | BAD_REQUEST | Invalid request format |
| 401 | UNAUTHORIZED | Missing or invalid token |
| 403 | FORBIDDEN | Insufficient permissions |
| 404 | NOT_FOUND | Resource not found |
| 409 | CONFLICT | Resource already exists |
| 422 | VALIDATION_ERROR | Request validation failed |
| 429 | RATE_LIMITED | Too many requests |
| 500 | INTERNAL_ERROR | Server error |
| 503 | SERVICE_UNAVAILABLE | Service temporarily unavailable |
## Pagination
### Request
```
GET /api/v1/vms?limit=20&offset=40
```
### Parameters
| Param | Type | Default | Max | Description |
|-------|------|---------|-----|-------------|
| limit | int | 20 | 100 | Items per page |
| offset | int | 0 | - | Skip N items |
## Filtering
### Query Parameters
```
GET /api/v1/vms?status=running&project_id=proj-123
```
### Prefix Search (KV)
```
GET /api/v1/kv?prefix=config/
```
## Port Convention
| Service | gRPC Port | HTTP Port |
|---------|-----------|-----------|
| ChainFire | 50051 | 8081 |
| FlareDB | 50052 | 8082 |
| IAM | 50053 | 8083 |
| PlasmaVMC | 50054 | 8084 |
| k8shost | 50055 | 8085 |
| LightningSTOR | 50056 | 8086 |
| CreditService | 50057 | 8087 |
| PrismNET | 50058 | 8088 |
| NightLight | 50059 | 8089 |
| FiberLB | 50060 | 8090 |
| FlashDNS | 50061 | 8091 |
## Service-Specific Endpoints
### ChainFire (KV Store)
```
GET /api/v1/kv/{key} # Get value
PUT /api/v1/kv/{key} # Put value {"value": "..."}
DELETE /api/v1/kv/{key} # Delete key
GET /api/v1/kv?prefix={prefix} # Range scan
GET /api/v1/cluster/status # Cluster health
POST /api/v1/cluster/members # Add member
```
### FlareDB (Database)
```
POST /api/v1/sql # Execute SQL {"query": "SELECT ..."}
GET /api/v1/tables # List tables
GET /api/v1/kv/{key} # KV get
PUT /api/v1/kv/{key} # KV put
GET /api/v1/scan?start={}&end={} # Range scan
```
### IAM (Authentication)
```
POST /api/v1/auth/token # Get token
POST /api/v1/auth/verify # Verify token
GET /api/v1/users # List users
POST /api/v1/users # Create user
GET /api/v1/users/{id} # Get user
DELETE /api/v1/users/{id} # Delete user
GET /api/v1/projects # List projects
POST /api/v1/projects # Create project
```
### PlasmaVMC (VM Management)
```
GET /api/v1/vms # List VMs
POST /api/v1/vms # Create VM
GET /api/v1/vms/{id} # Get VM
DELETE /api/v1/vms/{id} # Delete VM
POST /api/v1/vms/{id}/start # Start VM
POST /api/v1/vms/{id}/stop # Stop VM
POST /api/v1/vms/{id}/reboot # Reboot VM
GET /api/v1/vms/{id}/console # Get console URL
```
### k8shost (Kubernetes)
```
GET /api/v1/pods # List pods
POST /api/v1/pods # Create pod
GET /api/v1/pods/{name} # Get pod
DELETE /api/v1/pods/{name} # Delete pod
GET /api/v1/services # List services
POST /api/v1/services # Create service
GET /api/v1/nodes # List nodes
```
### CreditService (Billing)
```
GET /api/v1/wallets/{project_id} # Get wallet balance
POST /api/v1/wallets/{project_id}/reserve # Reserve credits
POST /api/v1/wallets/{project_id}/commit # Commit reservation
POST /api/v1/wallets/{project_id}/release # Release reservation
GET /api/v1/quotas/{project_id} # Get quotas
PUT /api/v1/quotas/{project_id} # Set quotas
```
### NightLight (Metrics) - Already HTTP
```
POST /api/v1/write # Push metrics (Prometheus remote write)
GET /api/v1/query?query={promql} # Instant query
GET /api/v1/query_range?query={}&start={}&end={}&step={} # Range query
GET /api/v1/series?match[]={}&start={}&end={} # Series metadata
GET /api/v1/labels # List labels
```
### LightningSTOR (S3) - Already HTTP
```
# S3-compatible (different port/path)
PUT /{bucket} # Create bucket
DELETE /{bucket} # Delete bucket
GET / # List buckets
PUT /{bucket}/{key} # Put object
GET /{bucket}/{key} # Get object
DELETE /{bucket}/{key} # Delete object
GET /{bucket}?list-type=2 # List objects
```
## curl Examples
### ChainFire KV
```bash
# Put
curl -X PUT http://localhost:8081/api/v1/kv/mykey \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d '{"value": "hello world"}'
# Get
curl http://localhost:8081/api/v1/kv/mykey \
-H "Authorization: Bearer $TOKEN"
# Delete
curl -X DELETE http://localhost:8081/api/v1/kv/mykey \
-H "Authorization: Bearer $TOKEN"
```
### PlasmaVMC
```bash
# Create VM
curl -X POST http://localhost:8084/api/v1/vms \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d '{
"name": "my-vm",
"cpu_cores": 2,
"memory_mb": 4096,
"disk_gb": 20,
"image": "ubuntu-22.04"
}'
# List VMs
curl http://localhost:8084/api/v1/vms \
-H "Authorization: Bearer $TOKEN"
# Start VM
curl -X POST http://localhost:8084/api/v1/vms/vm-123/start \
-H "Authorization: Bearer $TOKEN"
```
### FlareDB SQL
```bash
# Execute query
curl -X POST http://localhost:8082/api/v1/sql \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d '{"query": "SELECT * FROM users WHERE id = 1"}'
```
## Implementation Notes
### Rust Framework
Use `axum` (already in most services):
```rust
use axum::{
routing::{get, post, put, delete},
Router, Json, extract::Path,
};
let app = Router::new()
.route("/api/v1/kv/:key", get(get_kv).put(put_kv).delete(delete_kv))
.route("/api/v1/cluster/status", get(cluster_status));
```
### Run Alongside gRPC
```rust
// Start both servers
tokio::select! {
_ = grpc_server.serve(grpc_addr) => {},
_ = axum::Server::bind(&http_addr).serve(app.into_make_service()) => {},
}
```
### Error Mapping
```rust
impl From<ServiceError> for HttpError {
fn from(e: ServiceError) -> Self {
match e {
ServiceError::NotFound(_) => HttpError::not_found(e.to_string()),
ServiceError::AlreadyExists(_) => HttpError::conflict(e.to_string()),
ServiceError::InvalidArgument(_) => HttpError::bad_request(e.to_string()),
_ => HttpError::internal(e.to_string()),
}
}
}
```
## References
- T050 Task: docs/por/T050-rest-api/task.yaml
- PROJECT.md: 統一API/仕様
- Existing HTTP: NightLight (metrics), LightningSTOR (S3)