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

9 KiB

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:

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

{
  "field1": "value1",
  "field2": 123
}

Response Format

Success Response

{
  "data": {
    // Resource data
  },
  "meta": {
    "request_id": "req-abc123",
    "timestamp": "2025-12-12T01:40:00Z"
  }
}

List Response

{
  "data": [
    { "id": "item-1", ... },
    { "id": "item-2", ... }
  ],
  "meta": {
    "total": 42,
    "limit": 20,
    "offset": 0,
    "request_id": "req-abc123"
  }
}

Error Response

{
  "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

# 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

# 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

# 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):

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

// Start both servers
tokio::select! {
    _ = grpc_server.serve(grpc_addr) => {},
    _ = axum::Server::bind(&http_addr).serve(app.into_make_service()) => {},
}

Error Mapping

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)