# API Standards ## Philosophy Two API surfaces: Go REST API (apiserver) and C daemon messaging (Cap'n Proto over TCP). Both follow a consistent pattern: validate at the boundary, delegate business logic, return structured responses. Non-blocking I/O throughout the daemon layer. ## Go REST API (apiserver) ### Handler Pattern Every resource follows a single consistent pattern: ```go // {Resource}Controller struct type JobController struct{} // {Action}{Resource} handler method func (j JobController) SubmitJob(ctx *gin.Context) { // 1. Validate input at the boundary req, err := preCheckSubmitJobRequest(ctx) if err != nil { ReturnError(ctx, common.INVALID_PARAMS_CODE, err.Error(), nil) return } // 2. Delegate to service layer result, err := j.svc.Submit(req) if err != nil { ReturnError(ctx, common.ERROR_CODE, err.Error(), nil) return } // 3. Return structured success ReturnSuccess(ctx, common.SUCCESS_CODE, "submitted", result) } ``` **Rules**: - Early return on every error — `ReturnError(...); return` - Keep `preCheck*` functions in the same file as the handler - Controllers delegate to services — business logic never lives in handlers - Dependencies flow inward: `controllers → services → store/backend` ### Response Format Every HTTP response uses a consistent envelope via two helpers in `controllers/response.go`: ```go // Success (always HTTP 200) ReturnSuccess(ctx, code, msg, data) // → {code: 200, msg: "success", data: {...}} // Error (HTTP status == code parameter) ReturnError(ctx, code, msg, details) // → {code: 401, msg: "access forbidden", details: nil} ``` ### Standard Error Codes All codes defined in `common/common.go`: | Code | Constant | Usage | |------|----------|-------| | 200 | `SUCCESS_CODE` | All success responses | | 400 | `INVALID_PARAMS_CODE` | Malformed/missing input | | 401 | `ERROR_AUTH_CHECK_TOKEN_CODE` | Missing/invalid JWT | | 403 | `ERROR_AUTH_CODE` | Valid JWT, insufficient permission | | 404 | `ERROR_NOT_FOUND_CODE` | Resource not found | | 429 | `ERROR_TOO_MANY_REQUESTS_CODE` | Rate limit exceeded | | 500 | `ERROR_CODE` | Unexpected server error | ### Middleware Chain Order is defined in `router/routers.go` and is **non-negotiable**: ``` WhitelistURL → WhitelistIP → RateLimit → ValidateHandler (JWT) → CORS → RBAC ``` - **WhitelistURL**: filter by allowed paths (applied first for efficiency) - **WhitelistIP**: abort 403 if client IP not in whitelist - **RateLimit**: 1000 req/sec, burst 2000 (global config, `config/app.yaml`) - **ValidateHandler**: parse and validate JWT from `Authorization: Bearer` header; validate body content-type - **CORS**: origin checks via config - **RBAC**: `RequirePermission("action:scope")` per-route, enforced by `controllers/rbac_check.go` ### Permission Format `"action:scope"` — scopes: `all`, `self`, `cluster`, `hostgroup`, `self_ug`, `self_hg`. Defined in `config/rbac.yaml`. Roles support inheritance via `includes:`. ## C Daemon Messaging ### Cap'n Proto Wire Format All inter-daemon messages are Cap'n Proto serialized. Schema: `src/schema/controller.capnp`. **Request** (`xce_request_t`): - `opcode` — OPCode enum (43 values) - `seq` — request sequence number - `from` — sender peer info (host/key) - `body` — union of 29 message types (including the empty `nomsg` placeholder) **Reply** (`xce_reply_t`): - `opcode` — echoes the originating request's opcode by default (`init_reply_by_req()` sets `reply->opcode = req->opcode`); many call sites override it with a type-specific opcode (e.g. `OPCode_preVoteReply`, `OPCode_walAck`, `OPCode_schedAdmin`). Consumers dispatch on this value, so it is not a fixed `OPCode_reply`/`OPCode_redirect` pair — that pairing only applies to the separate wrapper `xce_request_t` that `xce_reply()` builds internally. - `status` — error code (0 = success) - `seq` — echoes request seq - `data` — union of reply payloads (clusterconf, jobid, stateReport, walackreplymsg, etc.) ### Key OPCodes | Group | OPCodes | Purpose | |-------|---------|---------| | Control | `jobsub (11)`, `jobdispatch (8)`, `jobcontrol (15)` | Job lifecycle | | Cluster | `hello (2)`, `join (3)`, `clusterconf (17)`, `heartbeat (25)` | Membership and liveness | | Replication | `replConfig (29)`, `replConfigPush (30)`, `walCatchupEntry (37)`, `snapshotTransfer (36)` | Config + WAL replication | | Vote | `requestVote`, reply | Raft election | | Data | `push (6)`, `get (5)`, `notify (10)` | Data exchange | ### Daemon I/O Functions ```c // Enqueue request to endpoint send buffer (non-blocking, always) int xce_request(endpoint_t *ep, xce_request_t *req); // Enqueue reply to inbound request sender (non-blocking) int xce_reply(endpoint_t *ep, xce_peerinfo_t *host, xce_reply_t *reply); // Cap'n Proto serialize request to packet int xce_encode(xce_request_t *req, io_packet_t *pkt); // Cap'n Proto deserialize packet to request int xce_decode(io_packet_t *pkt, xce_request_t *req); ``` **msg_handler callback**: Each endpoint has `ep->msg_handler` — a function pointer `int (*)(void*)` called by iomgr when a packet arrives. Handlers must return quickly (vtask context). Registered per-endpoint during connection setup. ### Adding a New Opcode 1. Add the opcode constant to the `OPCode` enum in `src/schema/controller.capnp` 2. Add the message struct to `src/schema/controller.capnp` (Request/Reply union arms) 3. Rebuild capnp schemas (automatic via meson) 4. Add a `case OPCode_...:` in the `switch (request.opcode)` dispatch inside `myreq_handler()` in `cluster.c` (this runs after `ha_dispatch_gate`, which only applies a role-based allow/deny filter, not per-opcode handler logic) 5. Add encode/decode paths in `src/lib/request.c` if using raw payloads 6. **If the new opcode is primary-only** (absent from `ha_dispatch_gate`'s `CTRL_STANDBY` allow-list), confirm its rejection reply reaches a client that can actually recover — `myreq_handler`'s generic gate-rejection path (`cluster.c`, right after the `ha_dispatch_gate()` call) must populate the redirect fields via `fill_redirect_reply()` (`join.c`) when rejecting for `-XCE_ERR_STANDBY`, mirroring `OPCode_join`'s handler. Every client-side redirect-follower in this codebase (`xceadmin.discovery.follow_redirects()`, libxce's own failover path) is already generic/opcode-agnostic and works automatically the moment the server supplies a redirect — don't assume "the client should just retry" is a client-side gap without first checking whether the server ever gave it anything to retry against (bug-1700: the identical gap was independently found and documented for `OPCode_upgradeConfig` first — `test_e2e_live_ha_push.py`'s module docstring — then shipped again under `OPCode_schedAdmin` before either was actually fixed).