feat(quotas+billing): implement the users/quota milestone (P9)#10
Merged
Conversation
Per-user resource quotas with pay-as-you-go hourly billing. Quotas: - user_quotas table (00006): admin-set per-user overrides of max servers / cpus / memory_mb, 0 = unlimited per axis; users without a row fall back to the configurable system default (QUOTA_MAX_*). - Enforced at server create: ServerHandler.Create checks the owner's current usage (GameServerRepository.OwnerUsage, counting every live server) against their effective quota (model.UserQuota.Allows) and returns 403 with the breached dimension before placement. - Endpoints: GET /quota (self), GET/PUT/DELETE /admin/users/:id/quota. Billing (pay-as-you-go, hourly): - billing_usage table (00007): a metered ledger. The reconciler opens an interval on MarkRunning and closes it on stop/lost/delete; both idempotent (partial unique index = one open interval per server), so retries can't double-bill. internal/billing holds the pure pricing (per-vCPU-hour + per-GB-hour, BILLING_*); BillingRepository.OwnerLedger aggregates month-to-date with open intervals measured to now. - Endpoints: GET /billing (self), GET /admin/users/:id/billing — line items, period total, and live hourly burn. Frontend: the Quotas & Users view (was a stub) is built — usage-vs-limit meters, inline quota editing + reset-to-default, a pay-as-you-go cost column with live burn, and fleet burn/spend tiles. Verify: unit tests for UserQuota.Allows and billing pricing; e2e (quota_test.go, billing_test.go) covering exceed-quota -> 403, admin set/get/delete + authorization, and the meter opening/closing across a server's run/stop lifecycle. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
fca698d to
4543a22
Compare
Owner-scoped player management: a user maintains a roster of players (by username) and chooses, per player, which of their own servers each may use — check/uncheck access. - Schema (migration 00008): players (unique per owner+username) and a player_servers many-to-many grant; grants onto soft-deleted servers are filtered out on read. - model.Player + Minecraft-style username validation (3-16 chars, [A-Za-z0-9_]). - PlayerRepository: transactional create/update (username + full grant-set replace), grants aggregated as a live-server-filtered array on read. - Owner-scoped API /players (CRUD): grant sets are validated to be the caller's own live servers (cross-owner ids -> 400), duplicate username -> 409, ownership-scoped reads (no existence leak). - Frontend: Players view (new nav entry) — add/remove players, search, and a server checklist per player that toggles grants live. Verify: unit test for username validation; e2e (players_test.go) covering the roster + grant lifecycle, validation, duplicate/conflict, and cross-owner isolation. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The platform now owns each server's in-game whitelist: the reconciler periodically feeds a running server's granted-player set to its workload over RCON, so checking/unchecking a player converges on the live server. Path (control plane -> agent -> guest), reusing the existing vsock control channel snapshots and health probing already use — no protobuf change, since the AgentLink Command is a generic op + JSON payload: - reconciler: a decoupled 30s sync loop pushes each running server's desired whitelist (PlayerRepository.UsernamesForServer) via a new WhitelistSource seam; failures are logged, never block the core tick. - provisioner/hub/agent: new SyncWhitelist op threaded through Provisioner, the Commander/Hub, and the agent Runtime (fake = no-op). - firecracker driver: forwards the desired set down the VM's vsock UDS. - guest init: a new WHITELIST control verb diffs the desired set against the live whitelist (read over RCON) and applies the minimal add/remove, enabling enforcement when non-empty and disabling it when empty — so a server with no granted players stays open. - internal/minecraft: pure ParseWhitelist + WhitelistSyncCommands (case-insensitive diff, deterministic order) for the guest to apply. Verify: unit tests for the parse/diff helpers and the agent op dispatch; e2e pins the reconciler's data source (UsernamesForServer) after grants. The live RCON application rides the KVM lane like the other vsock ops. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The e2e harness builds config.Config by hand rather than via config.Load, so cfg.Quota and cfg.Billing were zero-valued: the billing endpoints returned an empty currency and a 0 rate, and the default quota read as 0 (unlimited), failing TestBillingMetersRunningServer, TestBillingAdminView, and TestQuotaDefaultSelfView. Set both in the harness: a production-shaped billing price list (so a running server accrues a non-zero, currency-tagged bill) and a generous default quota — positive (so the default self-view reads as a real cap) but high enough that the global per-user limit never interferes with the capacity/placement/lifecycle suites; the quota-enforcement tests set their own small per-user overrides. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
QuotaRow read billing.* in the cost cell but only destructured user/quota/usage from row, breaking the frontend build with TS2304. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Per-user resource quotas with pay-as-you-go hourly billing.
Quotas:
servers / cpus / memory_mb, 0 = unlimited per axis; users without a
row fall back to the configurable system default (QUOTA_MAX_*).
current usage (GameServerRepository.OwnerUsage, counting every live
server) against their effective quota (model.UserQuota.Allows) and
returns 403 with the breached dimension before placement.
Billing (pay-as-you-go, hourly):
an interval on MarkRunning and closes it on stop/lost/delete; both
idempotent (partial unique index = one open interval per server), so
retries can't double-bill. internal/billing holds the pure pricing
(per-vCPU-hour + per-GB-hour, BILLING_*); BillingRepository.OwnerLedger
aggregates month-to-date with open intervals measured to now.
items, period total, and live hourly burn.
Frontend: the Quotas & Users view (was a stub) is built — usage-vs-limit
meters, inline quota editing + reset-to-default, a pay-as-you-go cost
column with live burn, and fleet burn/spend tiles.
Verify: unit tests for UserQuota.Allows and billing pricing; e2e
(quota_test.go, billing_test.go) covering exceed-quota -> 403, admin
set/get/delete + authorization, and the meter opening/closing across a
server's run/stop lifecycle.
Co-Authored-By: Claude Opus 4.8 (1M context) noreply@anthropic.com