Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
133 changes: 133 additions & 0 deletions .claude/commands/api-docs-sync.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
---
description: Scan the b3 API server against docs/platform/api and update the docs to match
argument-hint: "[path to b3 checkout] [--report-only]"
allowed-tools: Bash, Read, Edit, Write, Grep, Glob
---

# Sync `docs/platform/api` with the b3 API server

`$ARGUMENTS`

`b3` is the Go API server behind these docs. This command finds every place the
two have drifted and fixes the docs. Default b3 checkout:
`../b3` relative to this repo (i.e. `go.bytebuilders.dev/b3`). If `--report-only`
is passed, produce the ranked findings list and stop — change nothing.

Docs never drive code: **b3 is always the source of truth.** Never edit b3 to
match the docs, and never invent an endpoint the router does not register.

## 1. Extract the route table

```bash
B3=${1:-../b3}
python3 hack/api-doc-sync/extract_routes.py "$B3/routers/api/v1" > /tmp/routes.txt
python3 hack/api-doc-sync/compare.py /tmp/routes.txt docs/platform/api \
docs/platform/api/openapi.yaml > /tmp/api-diff.txt
```

`extract_routes.py` walks the registration call graph from `RegisterRoutes` and
`RegisterMarketplaceServiceRoutes`, so a helper called inside
`m.Group("/user", …)` correctly gets the `/user` prefix. Read its docstring
before trusting an odd-looking path.

**Baseline:** the last full run extracted **547 routes** (545 unique
method+path) and ended with complete md and openapi.yaml coverage.

**Sanity gate:** if the route count dropped sharply versus the baseline, or many
paths look truncated, the extractor has stopped understanding a new registration
style (a new `m.Combo` chain shape, a `m.Group` whose path is a constant, a
helper invoked as `pkg.Register(m)` rather than `register(m)`). Fix the
extractor first — a silently short route table produces confidently wrong docs.

## 2. Triage the diff

Work the sections in this order; the first three are actionable, the last is
advisory.

1. **in openapi.yaml but NOT in code** — a documented path that no longer
exists, or a *wrong* path. Before deleting anything, grep the code for the
handler name: a "missing" path is usually a real endpoint under a different
prefix (a helper moved inside another `m.Group`), and the fix is to rename the
path, not drop it.
2. **in CODE but NOT in md** — undocumented endpoints. Write them up (step 3).
3. **in CODE but NOT in openapi.yaml** — add the path/operation to the spec.
4. **in md but NOT in code** — advisory. Overview tables put several verbs on one
line (`| GET/POST/DELETE |`) and curl examples contain concrete values, so
most entries here are noise. Only chase one if it names a plausible endpoint
that `grep` cannot find in b3 at all.

Known-good false positive — leave it alone:

- `ANY /*` — the `/api/v1` catch-all 404 handler (`miscellaneous.go`). It is not
an endpoint and is deliberately absent from the docs and the spec. As of the
last run it is the **only** entry in both code→md and code→openapi.

`compare.py` already resolves the doc conventions this repo uses: absolute
headings, headings relative to a page's declared root ("All routes on this page
are rooted at `/api/v1/user/contracts`"), several verbs sharing one path
(`PUT · PATCH /x`, `| POST/GET/DELETE |`), `/api/v1`-prefixed table entries, and
abbreviated `### GET .../kubeDb/views/x` headings (matched as a path suffix). If a
new page invents another convention, teach the tool rather than rewriting the
page — but a page whose relative headings state no root cannot be matched, so
that page does need a root sentence.

For each surviving finding, verify against the source before writing a word:
open the `file:line` the extractor printed, read the handler, and derive the
request/response shape from the bound payload type (`binding.Json(X{})` /
`bind(X{})`) and what the handler actually writes (`ctx.JSON(...)`). Cite what
you read. If a shape cannot be determined from the code, say so on the page
instead of inventing fields.

## 3. Update the docs

- Put each endpoint on the page for its API group — the group table in
`docs/platform/api/README.md` maps base paths to pages. Match the
surrounding page's existing section style exactly (heading form, auth line,
path/query parameter tables, JSON examples, `Errors:` line, `curl` block).
- Respect each page's declared path root: if the page says paths are relative to
`/api/v1/user/contracts`, write `### GET /{id}`, not the absolute path.
- Middleware → prose mapping, as the existing pages use it: `reqToken()` →
"token required"; `reqSiteAdmin()` / `authzCheck(...:site_admin)` → site admin;
`reqOrgFromQuery()` → resolves the org from `?org=`; `reqClusterAssignment()`
→ owner+cluster resolved and a Kubernetes client built; `authzCheck(X, "perm")`
→ name the permission string.
- Note availability when the registration is conditional in
`routers/api/v1/api.go`: `setting.AppsCodeHosted` → "AppsCode-hosted only";
`setting.IsBillingEnabled()` → "billing-enabled deployments only".
- Only add a `> **Verified:** …` note if you actually ran the request against a
live deployment in this session. Never copy one from another endpoint.
- Update `docs/platform/api/openapi.yaml` alongside the md: add/rename the path,
reuse existing `components/schemas` and `parameters` rather than duplicating,
and tag the operation with the same tag its group's siblings use.
- If the API-group set itself changed (a whole new `register*APIs` function),
add a row to the group table and a section page — do not bury a new group
inside an unrelated page.

## 4. Regenerate and verify

```bash
python3 hack/api-doc-sync/refresh_reference.py # re-inlines the spec into reference/api.html
python3 -c "import yaml;yaml.safe_load(open('docs/platform/api/openapi.yaml'))"
liche -p -h -l -s <each changed md file> # exactly as CI runs it
python3 hack/api-doc-sync/compare.py /tmp/routes.txt docs/platform/api \
docs/platform/api/openapi.yaml | head -40
```

The `liche` invocation must keep `-s`; this repo's links intentionally carry one
extra `../` (see `CLAUDE.md`). Never "fix" a relative link to match the on-disk
path: a sibling page in the same directory is `../sibling.md`, and a page in
another directory is `../../other-dir/page.md`.

`docs/platform/api/README.md` fails this check by design — its links use
non-standard paths, which is why CI's liche fork passes `-i '^README\.md$'`. Add
that flag when scanning recursively; the locally installed `liche` may not have
it, in which case just skip `README.md`.

Re-run `compare.py` at the end and confirm the code→md and code→openapi lists
contain nothing but the known false positives above.

## 5. Report

Give a ranked summary: wrong paths first, then missing endpoints, then spec-only
changes. For anything you chose not to change, say why in one line. Note the
route count so the next run can compare.
2 changes: 1 addition & 1 deletion docs/platform/api/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ The server supports several authentication mechanisms:
| Session cookie | Web console | Cookie-based sign-in; CSRF-protected |
| Personal access token / Bearer token | API clients, CLI | `Authorization: token <t>`, `?token=`, `?access_token=` |
| Basic auth | Token management endpoints | With optional OTP (2FA) |
| OAuth2 / OIDC | SSO; the KubeDB Platform API Server is both provider and consumer | `/login/oauth/*`, `/.well-known/openid-configuration` |
| OAuth2 / OIDC | SSO; the KubeDB Platform API Server is both provider and consumer | `/accounts/login/oauth/*`, `/accounts/.well-known/openid-configuration` |
| LDAP / PAM | Enterprise sign-in sources | Configured by site admins |
| 2FA / WebAuthn | User accounts | TOTP, scratch tokens, security keys |
| License-based auth | Member clusters | Clusters authenticate with issued licenses / cluster tokens |
Expand Down
67 changes: 67 additions & 0 deletions docs/platform/api/billing-dashboard/usage-reports.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,73 @@ Common path parameters for the `summary` routes:
| `year` | string | Four-digit report year (e.g. `2026`). |
| `month` | string | Report month (e.g. `06` or `June`, per the generated-months list). |

## Monthly summary

### GET /dashboard/monthly-summary/{resourceType}

Returns the per-month usage totals for one product, newest month first, with
month-over-month change percentages.

- **Auth:** token; site-admin (`view_usage_analytics:site_admin`). Requires `?org=`.
- **Path parameters:**

| Name | Type | Description |
|---|---|---|
| `resourceType` | string | Product alias: `kubedb`, `stash`, `vault`, or `voyager`. |

- **Query parameters:**

| Name | Type | Required | Description |
|---|---|---|---|
| `org` | string | yes | Organization slug. |
| `period_start` | string | no | First month to include, `YYYY-MM`. Defaults to five months before the current month (a six-month window). |
| `period_end` | string | no | Last month to include, `YYYY-MM`. Only read when `period_start` is given; defaults to the current month. |

When an explicit period is given it is clamped to the months that actually have a
generated summary; if it overlaps none, the response comes back with an empty
`months` array. The requested range must not exceed **24 months**.

- **Response:** `200 OK` — a `MonthlySummaryResponse`:

```json
{
"resourceType": "kubedb",
"months": [
{
"year": 2026,
"month": 6,
"cpuCoreMonth": 12.5,
"memoryGiBMonth": 48.25,
"instances": 7,
"cpuChangeFromPrev": 4.2,
"memoryChangeFromPrev": -1.8
},
{
"year": 2026,
"month": 5,
"cpuCoreMonth": 12.0,
"memoryGiBMonth": 49.1,
"instances": 7
}
]
}
```

| Field | Type | Description |
|---|---|---|
| `resourceType` | string | Echo of the path parameter. |
| `months` | array | Monthly rows, most recent first. |
| `months[].year` | integer | Calendar year. |
| `months[].month` | integer | Calendar month, `1`–`12`. |
| `months[].cpuCoreMonth` | number | CPU core-months consumed. |
| `months[].memoryGiBMonth` | number | Memory GiB-months consumed. |
| `months[].instances` | integer | Instance count for the month. |
| `months[].cpuChangeFromPrev` | number | Percentage change vs. the previous (older) month; omitted on the oldest row. |
| `months[].memoryChangeFromPrev` | number | Percentage change vs. the previous (older) month; omitted on the oldest row. |

Errors: `400` for an unknown `resourceType`, a `period_start`/`period_end` that is not
`YYYY-MM`, a `period_start` after `period_end`, or a range longer than 24 months.

## Generated months

### GET /dashboard/summary/generated-months
Expand Down
51 changes: 25 additions & 26 deletions docs/platform/api/billing-dashboard/user-dashboard.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,21 +12,20 @@ section_menu_id: api

# User Billing Dashboard

Owner-scoped endpoints under `/api/v1/dashboard/clusters` that back the self-service
Owner-scoped endpoints under `/api/v1/user/dashboard/clusters` that back the self-service
billing dashboard an organization sees for its **own** clusters: active clusters,
their licenses and licensed products, and per-cluster / per-license / per-resource
event counts.

All paths on this page are relative to `/api/v1`. Every endpoint requires
`Authorization: token <YOUR_TOKEN>`, resolves the owner from the `org` query
parameter (`?org=<org-slug>`), and requires the `view:contracts` permission on that
organization (this is the org's "hosted mode / view contracts" grant — not
parameter (`?org=<org-slug>`), and requires the `view:licensed_clusters` permission on that
organization (the org's "hosted mode / view licensed clusters" grant — not
site-admin). This group is available only on **billing-enabled deployments**.

> **Verified:** every endpoint on this page returned `404 Not Found` against
> `appscode` on `<akp-host>` on 2026-07-14 — this deployment is not billing-enabled,
> so the `/dashboard/clusters/*` routes are not registered. (Sanity: `GET /version`
> and `GET /user` returned `200` with the same token.)
> **Note.** These paths were previously documented without the `/user` prefix.
> `registerBillingDashboardUserAPIs` registers `/dashboard/clusters` from inside the
> `/user` group, so the served prefix is `/api/v1/user/dashboard/clusters`.

Shared conventions on this page:

Expand All @@ -37,12 +36,12 @@ Shared conventions on this page:

## Clusters

### GET /dashboard/clusters/active
### GET /user/dashboard/clusters/active

Lists clusters that reported within the `limit` window for the owner resolved from
the query.

- **Auth:** token; owner-scoped (`view:contracts`). Requires `?org=`.
- **Auth:** token; owner-scoped (`view:licensed_clusters`). Requires `?org=`.
- **Query parameters:**

| Name | Type | Required | Description |
Expand All @@ -66,16 +65,16 @@ the query.

```
curl -H "Authorization: token $AKP_TOKEN" \
"https://<akp-host>/api/v1/dashboard/clusters/active?org=appscode"
"https://<akp-host>/api/v1/user/dashboard/clusters/active?org=appscode"
```

> **Verified:** returned `404` against `appscode` — billing not enabled on this deployment.

### GET /dashboard/clusters/{cid}
### GET /user/dashboard/clusters/{cid}

Returns cluster information for a cluster owned by the request owner.

- **Auth:** token; owner-scoped (`view:contracts`). Requires `?org=`.
- **Auth:** token; owner-scoped (`view:licensed_clusters`). Requires `?org=`.
- **Path parameters:**

| Name | Type | Description |
Expand All @@ -89,11 +88,11 @@ Returns cluster information for a cluster owned by the request owner.

> **Verified:** returned `404` against `appscode` — billing not enabled.

### GET /dashboard/clusters/{cid}/events-count
### GET /user/dashboard/clusters/{cid}/events-count

Returns today's event count for the cluster.

- **Auth:** token; owner-scoped (`view:contracts`). Requires `?org=`.
- **Auth:** token; owner-scoped (`view:licensed_clusters`). Requires `?org=`.
- **Path parameters:** `cid` (string) — cluster UID.
- **Response:** `200 OK` — an `EventsCounterResponse`.

Expand All @@ -113,11 +112,11 @@ the license/resource-scoped counter endpoints below; `error` is set instead of

## Licenses

### GET /dashboard/clusters/{cid}/licenses/
### GET /user/dashboard/clusters/{cid}/licenses/

Lists licensed plans associated with the request owner and cluster.

- **Auth:** token; owner-scoped (`view:contracts`). Requires `?org=`.
- **Auth:** token; owner-scoped (`view:licensed_clusters`). Requires `?org=`.
- **Path parameters:** `cid` (string) — cluster UID.
- **Response:** `200 OK` — an array of `LicensedPlan` (see the Admin Billing
Dashboard page for the full shape).
Expand All @@ -138,11 +137,11 @@ Lists licensed plans associated with the request owner and cluster.

> **Verified:** returned `404` against `appscode` — billing not enabled.

### GET /dashboard/clusters/{cid}/licenses/{lid}
### GET /user/dashboard/clusters/{cid}/licenses/{lid}

Returns a licensed plan (API form) associated with the request owner.

- **Auth:** token; owner-scoped (`view:contracts`). Requires `?org=`.
- **Auth:** token; owner-scoped (`view:licensed_clusters`). Requires `?org=`.
- **Path parameters:** `cid` (string), `lid` (string) — license ID.
- **Response:** `200 OK` — a `LicensePlanApiForm`.

Expand All @@ -166,22 +165,22 @@ Returns a licensed plan (API form) associated with the request owner.

## Event counts and events

### GET /dashboard/clusters/{cid}/licenses/{lid}/products/{product}/events-count
### GET /user/dashboard/clusters/{cid}/licenses/{lid}/products/{product}/events-count

Returns today's event count for a license/product.

- **Auth:** token; owner-scoped (`view:contracts`). Requires `?org=`.
- **Auth:** token; owner-scoped (`view:licensed_clusters`). Requires `?org=`.
- **Path parameters:** `cid` (string), `lid` (string), `product` (string).
- **Response:** `200 OK` — an `EventsCounterResponse` (with `product` and
`licenseID` populated).

> **Verified:** returned `404` against `appscode` — billing not enabled.

### GET /dashboard/clusters/{cid}/licenses/{lid}/products/{product}/groups/{group}/resources/{resource}/{rid}/events-count
### GET /user/dashboard/clusters/{cid}/licenses/{lid}/products/{product}/groups/{group}/resources/{resource}/{rid}/events-count

Returns today's event count for a specific resource object.

- **Auth:** token; owner-scoped (`view:contracts`). Requires `?org=`.
- **Auth:** token; owner-scoped (`view:licensed_clusters`). Requires `?org=`.
- **Path parameters:**

| Name | Type | Description |
Expand Down Expand Up @@ -211,11 +210,11 @@ Returns today's event count for a specific resource object.

> **Verified:** returned `404` against `appscode` — billing not enabled.

### GET /dashboard/clusters/{cid}/licenses/{lid}/products/{product}/events/
### GET /user/dashboard/clusters/{cid}/licenses/{lid}/products/{product}/events/

Returns the tabular event list for the caller's cluster/license/product.

- **Auth:** token; owner-scoped (`view:contracts`). Requires `?org=`.
- **Auth:** token; owner-scoped (`view:licensed_clusters` plus `view:event_resources`). Requires `?org=`.
- **Path parameters:** `cid` (string), `lid` (string), `product` (string).
- **Query parameters:**

Expand All @@ -240,11 +239,11 @@ Returns the tabular event list for the caller's cluster/license/product.

> **Verified:** returned `404` against `appscode` — billing not enabled.

### GET /dashboard/clusters/{cid}/licenses/{lid}/products/{product}/events/raw-event
### GET /user/dashboard/clusters/{cid}/licenses/{lid}/products/{product}/events/raw-event

Returns the raw badger value for a specific event key/version.

- **Auth:** token; owner-scoped (`view:contracts`). Requires `?org=`.
- **Auth:** token; owner-scoped (`view:licensed_clusters` plus `view:event_resources`). Requires `?org=`.
- **Path parameters:** `cid` (string), `lid` (string), `product` (string).
- **Query parameters:**

Expand Down
Loading
Loading