From 936d907db50e1bbe81d16b196f34edab4aedbc44 Mon Sep 17 00:00:00 2001 From: Taylor Bantle Date: Wed, 19 Aug 2026 16:21:53 -0700 Subject: [PATCH 1/3] Update both OpenAPI specs from ld main and regenerate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Re-vendors specs/hosted-v1.yaml and specs/dolthub-v2.yaml from ld main (781c0ea3435). Hosted v1 gains three Deployment endpoints and four schemas: GET /deployments/{owner}/{deployment}/config getDeploymentConfig GET /deployments/{owner}/{deployment}/backups listDeploymentBackups POST /deployments/{owner}/{deployment}/disable disableDeployment plus Backup, ConfigSetting, DeploymentConfig, and DisableAccepted. The hand-written v1 overview picks all three up in its endpoint table, and the long-running-work section now covers disable — it returns 202 with the deployment in `stopping` and is polled the same way a create is — along with the warning that disabling tears down instances and storage. DoltHub v2 has no structural change: same 22 operations and 40 schemas. Its diff is the RequestId header being hoisted into a shared component and error responses being declared as application/problem+json. That content-type change exposed a generator bug. Error bodies were looked up under application/json only, so every error row lost its Problem schema link the moment the declaration became application/problem+json. The hosted spec already used problem+json, which means those rows have been blank since the hosted docs landed. Media-type lookups now go through a jsonBody() helper that accepts either, so both APIs link Problem again. Co-Authored-By: Claude Opus 5 (1M context) --- scripts/lib/openapi-docs.mjs | 22 +- .../content/products/dolthub/api/v2/models.md | 2 +- .../content/products/hosted/api/v1/README.md | 7 +- .../products/hosted/api/v1/deployment.md | 244 +++++++++-- .../content/products/hosted/api/v1/models.md | 51 ++- .../content/products/hosted/api/v1/user.md | 6 +- specs/dolthub-v2.yaml | 68 ++- specs/hosted-v1.yaml | 407 +++++++++++++++++- 8 files changed, 748 insertions(+), 59 deletions(-) diff --git a/scripts/lib/openapi-docs.mjs b/scripts/lib/openapi-docs.mjs index a5cf870..e0a8cad 100644 --- a/scripts/lib/openapi-docs.mjs +++ b/scripts/lib/openapi-docs.mjs @@ -88,6 +88,20 @@ function createRenderer(spec, { baseUrl, tokenPlaceholder, modelsHref }) { // Examples // ------------------------------------------------------------------------- + // The JSON body of a request or response. Error responses are served as + // `application/problem+json` (RFC 9457) rather than `application/json`, so + // looking only at the latter silently drops the Problem schema from every + // error row. Fall back to any JSON-suffixed media type. + function jsonBody(carrier) { + const content = carrier?.content; + if (!content) return undefined; + return ( + content["application/json"] ?? + content["application/problem+json"] ?? + Object.entries(content).find(([type]) => /\bjson\b|\+json$/.test(type))?.[1] + ); + } + // OpenAPI 3.1 permits a media type to carry named `examples`. Prefer the one // named `default`, else the first — a hand-written example beats anything // synthesized from the schema. @@ -102,7 +116,7 @@ function createRenderer(spec, { baseUrl, tokenPlaceholder, modelsHref }) { } function requestExample(operation) { - const media = operation.requestBody?.content?.["application/json"]; + const media = jsonBody(operation.requestBody); const authored = mediaTypeExample(media); if (authored !== undefined) return authored; @@ -178,7 +192,7 @@ function createRenderer(spec, { baseUrl, tokenPlaceholder, modelsHref }) { const rawResp = rawResponses[successCode]; const resp = rawResp.$ref ? resolveRef(rawResp.$ref) : rawResp; - const media = resp.content?.["application/json"]; + const media = jsonBody(resp); // An example authored on the response wins outright — it shows the whole // envelope, including any `meta`, exactly as the API returns it. @@ -247,7 +261,7 @@ function createRenderer(spec, { baseUrl, tokenPlaceholder, modelsHref }) { function requestBodySection(requestBody) { if (!requestBody) return ""; - const schema = requestBody.content?.["application/json"]?.schema; + const schema = jsonBody(requestBody)?.schema; if (!schema) return ""; const resolved = deref(schema); const required = resolved.required ?? []; @@ -286,7 +300,7 @@ function createRenderer(spec, { baseUrl, tokenPlaceholder, modelsHref }) { // Resolve top-level $ref (e.g. $ref: "#/components/responses/Unauthorized") const resp = rawResp.$ref ? resolveRef(rawResp.$ref) : rawResp; const desc = escapeMarkdown(resp.description ?? ""); - const schema = resp.content?.["application/json"]?.schema; + const schema = jsonBody(resp)?.schema; const name = responseSchemaName(schema); const schemaLink = name ? `[\`${name}\`](${modelsHref}#model-${name.replace("[]", "").toLowerCase()})` diff --git a/site/dolt/src/content/products/dolthub/api/v2/models.md b/site/dolt/src/content/products/dolthub/api/v2/models.md index aa9159d..fc7ab45 100644 --- a/site/dolt/src/content/products/dolthub/api/v2/models.md +++ b/site/dolt/src/content/products/dolthub/api/v2/models.md @@ -33,7 +33,7 @@ A structured error body returned for every non-2xx response, following RFC 9457 | Field | Type | Required | Description | |-------|------|----------|-------------| -| `type` | `string` | no | A URI identifying the problem type; when dereferenced it points at human-readable documentation for the error. | +| `type` | `string` | yes | A URI identifying the problem type; when dereferenced it points at human-readable documentation for the error. | | `title` | `string` | yes | A short, human-readable summary of the problem type. | | `status` | `integer` | yes | The HTTP status code, repeated in the body for convenience. | | `detail` | `string` | no | A human-readable explanation specific to this occurrence of the problem. | diff --git a/site/dolt/src/content/products/hosted/api/v1/README.md b/site/dolt/src/content/products/hosted/api/v1/README.md index 6102f9f..4920879 100644 --- a/site/dolt/src/content/products/hosted/api/v1/README.md +++ b/site/dolt/src/content/products/hosted/api/v1/README.md @@ -46,6 +46,9 @@ See [Authentication](/products/hosted/api/v1/authentication) for how to create a | **GET** | `/api/v1/deployments/{owner}` | [List an owner's deployments](/products/hosted/api/v1/deployment#listDeployments) | | **GET** | `/api/v1/deployments/{owner}/{deployment}` | [Get a deployment](/products/hosted/api/v1/deployment#getDeployment) | | **GET** | `/api/v1/deployments/{owner}/{deployment}/instances` | [List a deployment's instances](/products/hosted/api/v1/deployment#listDeploymentInstances) | +| **GET** | `/api/v1/deployments/{owner}/{deployment}/config` | [Get a deployment's configuration](/products/hosted/api/v1/deployment#getDeploymentConfig) | +| **GET** | `/api/v1/deployments/{owner}/{deployment}/backups` | [List a deployment's backups](/products/hosted/api/v1/deployment#listDeploymentBackups) | +| **POST** | `/api/v1/deployments/{owner}/{deployment}/disable` | [Disable a deployment](/products/hosted/api/v1/deployment#disableDeployment) | ## Response shape @@ -92,9 +95,11 @@ Every response, including successful ones, carries an `x-request-id` header, ech Creating a deployment returns `202 Accepted` with the deployment in its `starting` state — provisioning continues after the response. Poll [Get a deployment](/products/hosted/api/v1/deployment#getDeployment) until `state` becomes `started`. +[Disabling a deployment](/products/hosted/api/v1/deployment#disableDeployment) works the same way: `202 Accepted` with the deployment in `stopping`, then poll until `state` is `stopped`. + Deployment names are unique within an owner, which makes creates idempotent by name: retrying after an ambiguous failure returns `409 Conflict` rather than provisioning a second deployment. -> **Creating a deployment incurs cost.** +> **Creating a deployment incurs cost.** Disabling one tears down its instances and their storage — [take a backup first](/products/hosted/api/v1/deployment#listDeploymentBackups) if you want the data. ## Stability diff --git a/site/dolt/src/content/products/hosted/api/v1/deployment.md b/site/dolt/src/content/products/hosted/api/v1/deployment.md index b6b5a32..9ae844a 100644 --- a/site/dolt/src/content/products/hosted/api/v1/deployment.md +++ b/site/dolt/src/content/products/hosted/api/v1/deployment.md @@ -41,12 +41,12 @@ curl -X GET 'https://hosted.doltdb.com/api/v1/deployment-options?cloud=aws' \ | Status | Description | Schema | |--------|-------------|--------| | `200` | The available options, narrowed by the supplied parameters. | [`DeploymentOptions`](/products/hosted/api/v1/models#model-deploymentoptions) | -| `400` | The request was malformed or failed input validation. | | -| `401` | Authentication credentials were missing or invalid. | | -| `405` | The HTTP method is not supported for this resource. | | -| `422` | The request was well-formed but semantically invalid. | | -| `500` | An unexpected server error occurred. | | -| `503` | The service is temporarily unavailable. | | +| `400` | The request was malformed or failed input validation. | [`Problem`](/products/hosted/api/v1/models#model-problem) | +| `401` | Authentication credentials were missing or invalid. | [`Problem`](/products/hosted/api/v1/models#model-problem) | +| `405` | The HTTP method is not supported for this resource. | [`Problem`](/products/hosted/api/v1/models#model-problem) | +| `422` | The request was well-formed but semantically invalid. | [`Problem`](/products/hosted/api/v1/models#model-problem) | +| `500` | An unexpected server error occurred. | [`Problem`](/products/hosted/api/v1/models#model-problem) | +| `503` | The service is temporarily unavailable. | [`Problem`](/products/hosted/api/v1/models#model-problem) | **Example response `200`** @@ -129,14 +129,14 @@ curl -X POST 'https://hosted.doltdb.com/api/v1/deployments' \ | Status | Description | Schema | |--------|-------------|--------| | `202` | The deployment has been accepted and is provisioning. `state` is `starting`. | [`Deployment`](/products/hosted/api/v1/models#model-deployment) | -| `400` | The request was malformed or failed input validation. | | -| `401` | Authentication credentials were missing or invalid. | | -| `403` | Authenticated, but not permitted to perform this action. | | -| `405` | The HTTP method is not supported for this resource. | | -| `409` | The request conflicts with the current state of the resource (e.g. it already exists). | | -| `422` | The request was well-formed but semantically invalid. | | -| `500` | An unexpected server error occurred. | | -| `503` | The service is temporarily unavailable. | | +| `400` | The request was malformed or failed input validation. | [`Problem`](/products/hosted/api/v1/models#model-problem) | +| `401` | Authentication credentials were missing or invalid. | [`Problem`](/products/hosted/api/v1/models#model-problem) | +| `403` | Authenticated, but not permitted to perform this action. | [`Problem`](/products/hosted/api/v1/models#model-problem) | +| `405` | The HTTP method is not supported for this resource. | [`Problem`](/products/hosted/api/v1/models#model-problem) | +| `409` | The request conflicts with the current state of the resource (e.g. it already exists). | [`Problem`](/products/hosted/api/v1/models#model-problem) | +| `422` | The request was well-formed but semantically invalid. | [`Problem`](/products/hosted/api/v1/models#model-problem) | +| `500` | An unexpected server error occurred. | [`Problem`](/products/hosted/api/v1/models#model-problem) | +| `503` | The service is temporarily unavailable. | [`Problem`](/products/hosted/api/v1/models#model-problem) | **Example response `202`** @@ -194,12 +194,12 @@ curl -X GET 'https://hosted.doltdb.com/api/v1/deployments/{owner}' \ | Status | Description | Schema | |--------|-------------|--------| | `200` | The owner's deployments. | [`DeploymentSummary[]`](/products/hosted/api/v1/models#model-deploymentsummary) | -| `400` | The request was malformed or failed input validation. | | -| `401` | Authentication credentials were missing or invalid. | | -| `403` | Authenticated, but not permitted to perform this action. | | -| `404` | The requested resource does not exist. | | -| `405` | The HTTP method is not supported for this resource. | | -| `500` | An unexpected server error occurred. | | +| `400` | The request was malformed or failed input validation. | [`Problem`](/products/hosted/api/v1/models#model-problem) | +| `401` | Authentication credentials were missing or invalid. | [`Problem`](/products/hosted/api/v1/models#model-problem) | +| `403` | Authenticated, but not permitted to perform this action. | [`Problem`](/products/hosted/api/v1/models#model-problem) | +| `404` | The requested resource does not exist. | [`Problem`](/products/hosted/api/v1/models#model-problem) | +| `405` | The HTTP method is not supported for this resource. | [`Problem`](/products/hosted/api/v1/models#model-problem) | +| `500` | An unexpected server error occurred. | [`Problem`](/products/hosted/api/v1/models#model-problem) | **Example response `200`** @@ -255,12 +255,12 @@ curl -X GET 'https://hosted.doltdb.com/api/v1/deployments/{owner}/{deployment}' | Status | Description | Schema | |--------|-------------|--------| | `200` | The deployment. | [`Deployment`](/products/hosted/api/v1/models#model-deployment) | -| `400` | The request was malformed or failed input validation. | | -| `401` | Authentication credentials were missing or invalid. | | -| `404` | The requested resource does not exist. | | -| `405` | The HTTP method is not supported for this resource. | | -| `500` | An unexpected server error occurred. | | -| `503` | The service is temporarily unavailable. | | +| `400` | The request was malformed or failed input validation. | [`Problem`](/products/hosted/api/v1/models#model-problem) | +| `401` | Authentication credentials were missing or invalid. | [`Problem`](/products/hosted/api/v1/models#model-problem) | +| `404` | The requested resource does not exist. | [`Problem`](/products/hosted/api/v1/models#model-problem) | +| `405` | The HTTP method is not supported for this resource. | [`Problem`](/products/hosted/api/v1/models#model-problem) | +| `500` | An unexpected server error occurred. | [`Problem`](/products/hosted/api/v1/models#model-problem) | +| `503` | The service is temporarily unavailable. | [`Problem`](/products/hosted/api/v1/models#model-problem) | **Example response `200`** @@ -324,12 +324,12 @@ curl -X GET 'https://hosted.doltdb.com/api/v1/deployments/{owner}/{deployment}/i | Status | Description | Schema | |--------|-------------|--------| | `200` | The deployment's non-stopped instances. | [`DeploymentInstance[]`](/products/hosted/api/v1/models#model-deploymentinstance) | -| `400` | The request was malformed or failed input validation. | | -| `401` | Authentication credentials were missing or invalid. | | -| `404` | The requested resource does not exist. | | -| `405` | The HTTP method is not supported for this resource. | | -| `500` | An unexpected server error occurred. | | -| `503` | The service is temporarily unavailable. | | +| `400` | The request was malformed or failed input validation. | [`Problem`](/products/hosted/api/v1/models#model-problem) | +| `401` | Authentication credentials were missing or invalid. | [`Problem`](/products/hosted/api/v1/models#model-problem) | +| `404` | The requested resource does not exist. | [`Problem`](/products/hosted/api/v1/models#model-problem) | +| `405` | The HTTP method is not supported for this resource. | [`Problem`](/products/hosted/api/v1/models#model-problem) | +| `500` | An unexpected server error occurred. | [`Problem`](/products/hosted/api/v1/models#model-problem) | +| `503` | The service is temporarily unavailable. | [`Problem`](/products/hosted/api/v1/models#model-problem) | **Example response `200`** @@ -360,3 +360,183 @@ curl -X GET 'https://hosted.doltdb.com/api/v1/deployments/{owner}/{deployment}/i } ``` +--- + +## List a deployment's backups {#listDeploymentBackups} +GET /api/v1/deployments/{owner}/{deployment}/backups + +Returns the backups held for `{owner}/{deployment}`, newest first. Deleted backups are not included. + +The list is not paginated: a deployment's retained backups are a bounded set. + + +**Parameters** + +| Name | In | Type | Required | Description | +|------|----|------|----------|-------------| +| `owner` | path | string | yes | The user or organization that owns the deployment. 3–32 characters of letters, digits, hyphens, and underscores. | +| `deployment` | path | string | yes | The deployment name, unique within the owner. 3–32 characters of letters, digits, hyphens, and underscores. | + +**Example request** + +```sh +curl -X GET 'https://hosted.doltdb.com/api/v1/deployments/{owner}/{deployment}/backups' \ + -H 'Authorization: Bearer YOUR_TOKEN' +``` + +**Responses** + +| Status | Description | Schema | +|--------|-------------|--------| +| `200` | The deployment's backups. | [`Backup[]`](/products/hosted/api/v1/models#model-backup) | +| `400` | The request was malformed or failed input validation. | [`Problem`](/products/hosted/api/v1/models#model-problem) | +| `401` | Authentication credentials were missing or invalid. | [`Problem`](/products/hosted/api/v1/models#model-problem) | +| `404` | The requested resource does not exist. | [`Problem`](/products/hosted/api/v1/models#model-problem) | +| `405` | The HTTP method is not supported for this resource. | [`Problem`](/products/hosted/api/v1/models#model-problem) | +| `500` | An unexpected server error occurred. | [`Problem`](/products/hosted/api/v1/models#model-problem) | +| `503` | The service is temporarily unavailable. | [`Problem`](/products/hosted/api/v1/models#model-problem) | + +**Example response `200`** + +```json +{ + "data": [ + { + "id": "20260812T020000.000", + "databases": [ + "analytics", + "staging" + ], + "instance_index": 0, + "created_at": "2026-08-12T02:00:00Z" + }, + { + "id": "20260811T020000.000", + "databases": [ + "analytics", + "staging" + ], + "size_bytes": 1048576, + "instance_index": 0, + "created_at": "2026-08-11T02:00:00Z" + } + ] +} +``` + +--- + +## Get a deployment's configuration {#getDeploymentConfig} +GET /api/v1/deployments/{owner}/{deployment}/config + +Returns the deployment's effective database configuration: every setting Hosted supports, carrying the deployment's own value where it has overridden one and the default otherwise. This is what the deployment's Configuration page shows. + +`is_overridden` distinguishes the two, and `default` is always reported, so a caller can tell what has been changed and what it would revert to. + +Values are strings as stored, including numeric and boolean settings. + + +**Parameters** + +| Name | In | Type | Required | Description | +|------|----|------|----------|-------------| +| `owner` | path | string | yes | The user or organization that owns the deployment. 3–32 characters of letters, digits, hyphens, and underscores. | +| `deployment` | path | string | yes | The deployment name, unique within the owner. 3–32 characters of letters, digits, hyphens, and underscores. | + +**Example request** + +```sh +curl -X GET 'https://hosted.doltdb.com/api/v1/deployments/{owner}/{deployment}/config' \ + -H 'Authorization: Bearer YOUR_TOKEN' +``` + +**Responses** + +| Status | Description | Schema | +|--------|-------------|--------| +| `200` | The deployment's effective configuration — every supported setting, at the value it is running. | [`DeploymentConfig`](/products/hosted/api/v1/models#model-deploymentconfig) | +| `400` | The request was malformed or failed input validation. | [`Problem`](/products/hosted/api/v1/models#model-problem) | +| `401` | Authentication credentials were missing or invalid. | [`Problem`](/products/hosted/api/v1/models#model-problem) | +| `404` | The requested resource does not exist. | [`Problem`](/products/hosted/api/v1/models#model-problem) | +| `405` | The HTTP method is not supported for this resource. | [`Problem`](/products/hosted/api/v1/models#model-problem) | +| `500` | An unexpected server error occurred. | [`Problem`](/products/hosted/api/v1/models#model-problem) | +| `503` | The service is temporarily unavailable. | [`Problem`](/products/hosted/api/v1/models#model-problem) | + +**Example response `200`** + +```json +{ + "data": { + "settings": [ + { + "key": "listener_max_connections", + "value": "500", + "default": "100", + "is_overridden": true + }, + { + "key": "behavior_read_only", + "value": "false", + "default": "false", + "is_overridden": false + } + ] + } +} +``` + +--- + +## Disable a deployment {#disableDeployment} +POST /api/v1/deployments/{owner}/{deployment}/disable + +Shuts the deployment down, tearing down its instances and their storage. Returns `202` with the deployment in `stopping`; poll `GET /api/v1/deployments/{owner}/{deployment}` until `state` is `stopped`. + +**Take a backup first if you want the data.** + +The deployment itself is not deleted. It stays readable with `disabled_at` and `disabled_by` set — which is why this is a `POST` to an action rather than a `DELETE` — and can be brought back by adding an instance with `POST /api/v1/deployments/{owner}/{deployment}/instances`; give that request a `backup_name` to restore the data, or it comes back empty. + +Not idempotent: disabling a deployment that is already stopping or stopped returns `422`. The `202` only confirms acceptance — `GET` the deployment for its full state. + + +**Parameters** + +| Name | In | Type | Required | Description | +|------|----|------|----------|-------------| +| `owner` | path | string | yes | The user or organization that owns the deployment. 3–32 characters of letters, digits, hyphens, and underscores. | +| `deployment` | path | string | yes | The deployment name, unique within the owner. 3–32 characters of letters, digits, hyphens, and underscores. | + +**Example request** + +```sh +curl -X POST 'https://hosted.doltdb.com/api/v1/deployments/{owner}/{deployment}/disable' \ + -H 'Authorization: Bearer YOUR_TOKEN' \ + -H 'Content-Type: application/json' +``` + +**Responses** + +| Status | Description | Schema | +|--------|-------------|--------| +| `202` | The teardown has been accepted. `state` is `stopping`. | [`DisableAccepted`](/products/hosted/api/v1/models#model-disableaccepted) | +| `400` | The request was malformed or failed input validation. | [`Problem`](/products/hosted/api/v1/models#model-problem) | +| `401` | Authentication credentials were missing or invalid. | [`Problem`](/products/hosted/api/v1/models#model-problem) | +| `403` | Authenticated, but not permitted to perform this action. | [`Problem`](/products/hosted/api/v1/models#model-problem) | +| `404` | The requested resource does not exist. | [`Problem`](/products/hosted/api/v1/models#model-problem) | +| `405` | The HTTP method is not supported for this resource. | [`Problem`](/products/hosted/api/v1/models#model-problem) | +| `422` | The request was well-formed but semantically invalid. | [`Problem`](/products/hosted/api/v1/models#model-problem) | +| `500` | An unexpected server error occurred. | [`Problem`](/products/hosted/api/v1/models#model-problem) | +| `503` | The service is temporarily unavailable. | [`Problem`](/products/hosted/api/v1/models#model-problem) | + +**Example response `202`** + +```json +{ + "data": { + "owner": "acme", + "name": "analytics", + "state": "stopping" + } +} +``` + diff --git a/site/dolt/src/content/products/hosted/api/v1/models.md b/site/dolt/src/content/products/hosted/api/v1/models.md index 8a498e2..6581a30 100644 --- a/site/dolt/src/content/products/hosted/api/v1/models.md +++ b/site/dolt/src/content/products/hosted/api/v1/models.md @@ -113,6 +113,29 @@ A storage type a deployment's volume can use. --- +## ConfigSetting {#model-configsetting} +One database setting and the value this deployment runs it at. + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `key` | `string` | yes | The setting's name. | +| `value` | `string` | yes | The value in effect — the deployment's override when it has one, otherwise the default. A string even for numeric and boolean settings, as stored. | +| `default` | `string` | yes | The value this setting takes when not overridden, and what it reverts to if the override is removed. | +| `is_overridden` | `boolean` | yes | Whether the deployment has overridden this setting. When `false`, `value` equals `default`. | + +--- + +## DeploymentConfig {#model-deploymentconfig} +A deployment's effective configuration — every supported setting, with the value it is running at. + +Settings are wrapped in an object rather than returned as a bare list so the resource can gain fields without a breaking change. + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `settings` | `array` | yes | Every setting Hosted supports for this deployment, in the order the catalogue reports them. | + +--- + ## DeploymentInstance {#model-deploymentinstance} One instance backing a deployment. A deployment has a primary and, when it has read replicas, one instance per replica. @@ -178,6 +201,19 @@ The deployment's lifecycle state. `starting` covers both initial provisioning an --- +## Backup {#model-backup} +A stored backup of a deployment's databases. + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `id` | `string` | yes | The backup's identifier, unique within the deployment. Derived from the time it was taken. | +| `databases` | `array` | yes | The databases captured in this backup. Empty if the deployment had none at the time. | +| `size_bytes` | `integer` | no | The backup's size in bytes. Absent until it has been measured, which happens asynchronously after the backup is taken — so a recent backup legitimately has no size yet. | +| `instance_index` | `integer` | yes | The index of the deployment instance the backup was taken from. | +| `created_at` | `string` | yes | When the backup was taken. | + +--- + ## CloudProvider {#model-cloudprovider} The cloud the deployment runs in. @@ -243,6 +279,17 @@ This is deliberately not the same shape as `Deployment`. The list RPC returns a --- +## DisableAccepted {#model-disableaccepted} +Confirmation that a deployment's shutdown was accepted. Deliberately minimal: it reports only what is certain once the shutdown commits. `GET` the deployment for its full state. + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `owner` | `string` | yes | The user or organization that owns the deployment. | +| `name` | `string` | yes | The deployment name. | +| `state` | `string` | yes | The deployment's lifecycle state. `starting` covers both initial provisioning and a restart; poll this field to observe a create or a resize reaching `started`. | + +--- + ## Deployment {#model-deployment} A Hosted Dolt deployment. @@ -274,6 +321,6 @@ Provider-specific private-networking configuration (AWS PrivateLink, GCP Private | `caller_role` | `string` | yes | The authenticated caller's role on this deployment. Always present on a read, since a caller without at least read access cannot retrieve the deployment at all. | | `created_by` | `string` | no | The username of the user who created the deployment. | | `created_at` | `string` | yes | When the deployment was created. | -| `destroy_at` | `string` | no | When the deployment is scheduled to be destroyed. Absent unless a destroy has been scheduled. | -| `destroyed_by` | `string` | no | The username of the user who destroyed the deployment. Absent unless it has been destroyed. | +| `disabled_at` | `string` | no | When the deployment is scheduled to shut down. Absent unless it has been disabled. | +| `disabled_by` | `string` | no | The username of the user who disabled the deployment. Absent unless it has been disabled. | diff --git a/site/dolt/src/content/products/hosted/api/v1/user.md b/site/dolt/src/content/products/hosted/api/v1/user.md index 916cfe5..136b7bf 100644 --- a/site/dolt/src/content/products/hosted/api/v1/user.md +++ b/site/dolt/src/content/products/hosted/api/v1/user.md @@ -25,9 +25,9 @@ curl -X GET 'https://hosted.doltdb.com/api/v1/user' \ | Status | Description | Schema | |--------|-------------|--------| | `200` | The authenticated user's profile. | [`User`](/products/hosted/api/v1/models#model-user) | -| `401` | Authentication credentials were missing or invalid. | | -| `405` | The HTTP method is not supported for this resource. | | -| `500` | An unexpected server error occurred. | | +| `401` | Authentication credentials were missing or invalid. | [`Problem`](/products/hosted/api/v1/models#model-problem) | +| `405` | The HTTP method is not supported for this resource. | [`Problem`](/products/hosted/api/v1/models#model-problem) | +| `500` | An unexpected server error occurred. | [`Problem`](/products/hosted/api/v1/models#model-problem) | **Example response `200`** diff --git a/specs/dolthub-v2.yaml b/specs/dolthub-v2.yaml index 571e028..dc51f98 100644 --- a/specs/dolthub-v2.yaml +++ b/specs/dolthub-v2.yaml @@ -1885,6 +1885,17 @@ paths: $ref: "#/components/responses/InternalServerError" components: + headers: + RequestId: + description: >- + The request identifier — echoed on every response (including 2xx). Reuse the + inbound `x-request-id` from the caller / edge proxy when present; otherwise the + server mints a `req_` value. + schema: + type: string + examples: + - req_2f1c9e7a-3b6d-4c5e-8a9f-0d1e2f3a4b5c + # securitySchemes model the credential types the v2 auth layer accepts. Personal # access tokens and OAuth access tokens are both presented in the `Authorization` header. # The public API does not support browser session cookies. @@ -1943,6 +1954,10 @@ components: (Problem Details for HTTP APIs). This is the single error model for the entire v2 API — there are no ad-hoc error shapes. required: + # `type` is required even though RFC 9457 permits omitting it: this API always emits + # it, and leaving it optional made the spec disagree with the generated DTO (which + # marks it non-optional because of the `default`). + - type - title - status - code @@ -2051,6 +2066,11 @@ components: properties: data: type: [object, array] + # Required for `data` to actually be unconstrained. Without it, openapi-typescript + # emits `Record` — an object permitting no properties — and the + # composed `Envelope & { data: Resource }` becomes unassignable, so nothing can use + # the generated response type. + additionalProperties: true description: The primary response payload — a resource, or an array of resources for list endpoints. meta: $ref: "#/components/schemas/Meta" @@ -3479,72 +3499,100 @@ components: responses: BadRequest: description: The request was malformed or failed input validation. + headers: + x-request-id: + $ref: "#/components/headers/RequestId" content: - application/json: + application/problem+json: schema: $ref: "#/components/schemas/Problem" Unauthorized: description: Authentication credentials were missing or invalid. + headers: + x-request-id: + $ref: "#/components/headers/RequestId" content: - application/json: + application/problem+json: schema: $ref: "#/components/schemas/Problem" Forbidden: description: Authenticated, but not permitted to perform this action. + headers: + x-request-id: + $ref: "#/components/headers/RequestId" content: - application/json: + application/problem+json: schema: $ref: "#/components/schemas/Problem" NotFound: description: The requested resource does not exist. + headers: + x-request-id: + $ref: "#/components/headers/RequestId" content: - application/json: + application/problem+json: schema: $ref: "#/components/schemas/Problem" MethodNotAllowed: description: The HTTP method is not supported for this resource. headers: + x-request-id: + $ref: "#/components/headers/RequestId" Allow: description: The HTTP methods supported by this resource. schema: type: string content: - application/json: + application/problem+json: schema: $ref: "#/components/schemas/Problem" Conflict: description: The request conflicts with the current state of the resource (e.g. it already exists). + headers: + x-request-id: + $ref: "#/components/headers/RequestId" content: - application/json: + application/problem+json: schema: $ref: "#/components/schemas/Problem" UnprocessableEntity: description: The request was well-formed but semantically invalid. + headers: + x-request-id: + $ref: "#/components/headers/RequestId" content: - application/json: + application/problem+json: schema: $ref: "#/components/schemas/Problem" TooManyRequests: description: The client has exceeded its rate limit. headers: + x-request-id: + $ref: "#/components/headers/RequestId" Retry-After: description: The number of seconds to wait before retrying the request. schema: type: integer format: int32 content: - application/json: + application/problem+json: schema: $ref: "#/components/schemas/Problem" InternalServerError: description: An unexpected server error occurred. + headers: + x-request-id: + $ref: "#/components/headers/RequestId" content: - application/json: + application/problem+json: schema: $ref: "#/components/schemas/Problem" ServiceUnavailable: description: The service is temporarily unavailable. + headers: + x-request-id: + $ref: "#/components/headers/RequestId" content: - application/json: + application/problem+json: schema: $ref: "#/components/schemas/Problem" diff --git a/specs/hosted-v1.yaml b/specs/hosted-v1.yaml index 56bb91c..41945e6 100644 --- a/specs/hosted-v1.yaml +++ b/specs/hosted-v1.yaml @@ -598,6 +598,269 @@ paths: "503": $ref: "#/components/responses/ServiceUnavailable" + /api/v1/deployments/{owner}/{deployment}/backups: + get: + operationId: listDeploymentBackups + summary: List a deployment's backups. + description: >- + Returns the backups held for `{owner}/{deployment}`, newest first. Deleted backups + are not included. + + + The list is not paginated: a deployment's retained backups are a bounded set. + tags: + - Deployment + security: + - apiToken: [] + parameters: + - name: owner + in: path + required: true + description: >- + The user or organization that owns the deployment. 3–32 characters of letters, + digits, hyphens, and underscores. + schema: + type: string + pattern: "^[-a-zA-Z0-9_]{3,32}$" + example: acme + - name: deployment + in: path + required: true + description: >- + The deployment name, unique within the owner. 3–32 characters of letters, + digits, hyphens, and underscores. + schema: + type: string + pattern: "^[-a-zA-Z0-9_]{3,32}$" + example: analytics + responses: + "200": + description: The deployment's backups. + headers: + x-request-id: + $ref: "#/components/headers/RequestId" + content: + application/json: + schema: + allOf: + - $ref: "#/components/schemas/Envelope" + - type: object + required: + - data + properties: + data: + type: array + description: The deployment's backups. + items: + $ref: "#/components/schemas/Backup" + examples: + default: + summary: Two retained backups. + value: + data: + - id: 20260812T020000.000 + databases: + - analytics + - staging + instance_index: 0 + created_at: "2026-08-12T02:00:00Z" + - id: 20260811T020000.000 + databases: + - analytics + - staging + size_bytes: 1048576 + instance_index: 0 + created_at: "2026-08-11T02:00:00Z" + "400": + $ref: "#/components/responses/BadRequest" + "401": + $ref: "#/components/responses/Unauthorized" + "404": + $ref: "#/components/responses/NotFound" + "405": + $ref: "#/components/responses/MethodNotAllowed" + "500": + $ref: "#/components/responses/InternalServerError" + "503": + $ref: "#/components/responses/ServiceUnavailable" + + /api/v1/deployments/{owner}/{deployment}/config: + get: + operationId: getDeploymentConfig + summary: Get a deployment's configuration. + description: >- + Returns the deployment's effective database configuration: every setting Hosted + supports, carrying the deployment's own value where it has overridden one and the + default otherwise. This is what the deployment's Configuration page shows. + + + `is_overridden` distinguishes the two, and `default` is always reported, so a caller + can tell what has been changed and what it would revert to. + + + Values are strings as stored, including numeric and boolean settings. + tags: + - Deployment + security: + - apiToken: [] + parameters: + - name: owner + in: path + required: true + description: >- + The user or organization that owns the deployment. 3–32 characters of letters, + digits, hyphens, and underscores. + schema: + type: string + pattern: "^[-a-zA-Z0-9_]{3,32}$" + example: acme + - name: deployment + in: path + required: true + description: >- + The deployment name, unique within the owner. 3–32 characters of letters, + digits, hyphens, and underscores. + schema: + type: string + pattern: "^[-a-zA-Z0-9_]{3,32}$" + example: analytics + responses: + "200": + description: >- + The deployment's effective configuration — every supported setting, at the value + it is running. + headers: + x-request-id: + $ref: "#/components/headers/RequestId" + content: + application/json: + schema: + allOf: + - $ref: "#/components/schemas/Envelope" + - type: object + required: + - data + properties: + data: + $ref: "#/components/schemas/DeploymentConfig" + examples: + default: + summary: One overridden setting and one on its default. + value: + data: + settings: + - key: listener_max_connections + value: "500" + default: "100" + is_overridden: true + - key: behavior_read_only + value: "false" + default: "false" + is_overridden: false + "400": + $ref: "#/components/responses/BadRequest" + "401": + $ref: "#/components/responses/Unauthorized" + "404": + $ref: "#/components/responses/NotFound" + "405": + $ref: "#/components/responses/MethodNotAllowed" + "500": + $ref: "#/components/responses/InternalServerError" + "503": + $ref: "#/components/responses/ServiceUnavailable" + + /api/v1/deployments/{owner}/{deployment}/disable: + post: + operationId: disableDeployment + summary: Disable a deployment. + description: >- + Shuts the deployment down, tearing down its instances and their storage. Returns + `202` with the deployment in `stopping`; poll + `GET /api/v1/deployments/{owner}/{deployment}` until `state` is `stopped`. + + + **Take a backup first if you want the data.** + + + The deployment itself is not deleted. It stays readable with `disabled_at` and + `disabled_by` set — which is why this is a `POST` to an action rather than a + `DELETE` — and can be brought back by adding an instance with + `POST /api/v1/deployments/{owner}/{deployment}/instances`; give that request a + `backup_name` to restore the data, or it comes back empty. + + + Not idempotent: disabling a deployment that is already stopping or stopped returns + `422`. The `202` only confirms acceptance — `GET` the deployment for its full + state. + tags: + - Deployment + security: + - apiToken: [] + parameters: + - name: owner + in: path + required: true + description: >- + The user or organization that owns the deployment. 3–32 characters of letters, + digits, hyphens, and underscores. + schema: + type: string + pattern: "^[-a-zA-Z0-9_]{3,32}$" + example: acme + - name: deployment + in: path + required: true + description: >- + The deployment name, unique within the owner. 3–32 characters of letters, + digits, hyphens, and underscores. + schema: + type: string + pattern: "^[-a-zA-Z0-9_]{3,32}$" + example: analytics + responses: + "202": + description: >- + The teardown has been accepted. `state` is `stopping`. + headers: + x-request-id: + $ref: "#/components/headers/RequestId" + content: + application/json: + schema: + allOf: + - $ref: "#/components/schemas/Envelope" + - type: object + required: + - data + properties: + data: + $ref: "#/components/schemas/DisableAccepted" + examples: + default: + summary: A deployment accepted for teardown. + value: + data: + owner: acme + name: analytics + state: stopping + "400": + $ref: "#/components/responses/BadRequest" + "401": + $ref: "#/components/responses/Unauthorized" + "403": + $ref: "#/components/responses/Forbidden" + "404": + $ref: "#/components/responses/NotFound" + "405": + $ref: "#/components/responses/MethodNotAllowed" + "422": + $ref: "#/components/responses/UnprocessableEntity" + "500": + $ref: "#/components/responses/InternalServerError" + "503": + $ref: "#/components/responses/ServiceUnavailable" + components: headers: RequestId: @@ -905,6 +1168,65 @@ components: examples: - 0 + ConfigSetting: + type: object + title: ConfigSetting + description: >- + One database setting and the value this deployment runs it at. + required: + - key + - value + - default + - is_overridden + properties: + key: + type: string + description: The setting's name. + examples: + - listener_max_connections + value: + type: string + description: >- + The value in effect — the deployment's override when it has one, otherwise the + default. A string even for numeric and boolean settings, as stored. + examples: + - "500" + default: + type: string + description: >- + The value this setting takes when not overridden, and what it reverts to if the + override is removed. + examples: + - "100" + is_overridden: + type: boolean + description: >- + Whether the deployment has overridden this setting. When `false`, `value` equals + `default`. + examples: + - true + + DeploymentConfig: + type: object + title: DeploymentConfig + description: >- + A deployment's effective configuration — every supported setting, with the value it + is running at. + + + Settings are wrapped in an object rather than returned as a bare list so the + resource can gain fields without a breaking change. + required: + - settings + properties: + settings: + type: array + description: >- + Every setting Hosted supports for this deployment, in the order the catalogue + reports them. + items: + $ref: "#/components/schemas/ConfigSetting" + DeploymentInstance: type: object title: DeploymentInstance @@ -1123,6 +1445,55 @@ components: examples: - started + Backup: + type: object + title: Backup + description: A stored backup of a deployment's databases. + required: + - id + - databases + - instance_index + - created_at + properties: + id: + type: string + description: >- + The backup's identifier, unique within the deployment. Derived from the time it + was taken. + examples: + - 20260811T020000.000 + databases: + type: array + description: >- + The databases captured in this backup. Empty if the deployment had none at the + time. + items: + type: string + examples: + - ["analytics", "staging"] + size_bytes: + type: integer + format: int64 + description: >- + The backup's size in bytes. Absent until it has been measured, which happens + asynchronously after the backup is taken — so a recent backup legitimately has + no size yet. + examples: + - 1048576 + instance_index: + type: integer + format: int32 + description: >- + The index of the deployment instance the backup was taken from. + examples: + - 0 + created_at: + type: string + format: date-time + description: When the backup was taken. + examples: + - "2026-08-11T02:00:00Z" + CloudProvider: type: string title: CloudProvider @@ -1259,6 +1630,30 @@ components: examples: - "2026-08-10T02:00:00Z" + DisableAccepted: + type: object + title: DisableAccepted + description: >- + Confirmation that a deployment's shutdown was accepted. Deliberately minimal: it + reports only what is certain once the shutdown commits. `GET` the deployment for its + full state. + required: + - owner + - name + - state + properties: + owner: + type: string + description: The user or organization that owns the deployment. + examples: + - acme + name: + type: string + description: The deployment name. + examples: + - analytics + state: + $ref: "#/components/schemas/DeploymentState" Deployment: type: object title: Deployment @@ -1390,19 +1785,19 @@ components: description: When the deployment was created. examples: - "2026-07-01T18:22:04Z" - destroy_at: + disabled_at: type: string format: date-time description: >- - When the deployment is scheduled to be destroyed. Absent unless a destroy has - been scheduled. + When the deployment is scheduled to shut down. Absent unless it has been + disabled. examples: - "2026-09-01T00:00:00Z" - destroyed_by: + disabled_by: type: string description: >- - The username of the user who destroyed the deployment. Absent unless it has been - destroyed. + The username of the user who disabled the deployment. Absent unless it has been + disabled. examples: - acme-ops From 5a2d80435b912d8dca91783f58cf704cf2f9ec15 Mon Sep 17 00:00:00 2001 From: Taylor Bantle Date: Thu, 20 Aug 2026 11:07:24 -0700 Subject: [PATCH 2/3] Pick up the instance add/remove endpoints from ld main Re-vendors specs/hosted-v1.yaml from ld main (3654e2df4bd), which adds two Deployment endpoints and two schemas: POST /deployments/{owner}/{deployment}/instances addDeploymentInstance DELETE /deployments/{owner}/{deployment}/instances/{id} deleteDeploymentInstance plus AddInstanceRequest and InstanceDeleteAccepted. Hosted v1 is now 11 operations and 23 schemas. dolthub-v2.yaml is unchanged at this ld commit. Both endpoints are added to the hand-written overview's endpoint table, and Long-running work gains a paragraph for them. They return 202 like create and disable, but unlike those there is no per-instance state field to poll, so progress is observed through the instance list instead: an added replica is ready when it reports a host, and a removed one is gone when it drops off the list, which only reports instances that aren't stopped. This is the first DELETE in either generated API doc; its badge and its body-less curl example both render correctly. Co-Authored-By: Claude Opus 5 (1M context) --- .../content/products/hosted/api/v1/README.md | 4 + .../products/hosted/api/v1/deployment.md | 117 ++++++- .../content/products/hosted/api/v1/models.md | 24 +- specs/hosted-v1.yaml | 309 ++++++++++++++++-- 4 files changed, 425 insertions(+), 29 deletions(-) diff --git a/site/dolt/src/content/products/hosted/api/v1/README.md b/site/dolt/src/content/products/hosted/api/v1/README.md index 4920879..f833f40 100644 --- a/site/dolt/src/content/products/hosted/api/v1/README.md +++ b/site/dolt/src/content/products/hosted/api/v1/README.md @@ -46,6 +46,8 @@ See [Authentication](/products/hosted/api/v1/authentication) for how to create a | **GET** | `/api/v1/deployments/{owner}` | [List an owner's deployments](/products/hosted/api/v1/deployment#listDeployments) | | **GET** | `/api/v1/deployments/{owner}/{deployment}` | [Get a deployment](/products/hosted/api/v1/deployment#getDeployment) | | **GET** | `/api/v1/deployments/{owner}/{deployment}/instances` | [List a deployment's instances](/products/hosted/api/v1/deployment#listDeploymentInstances) | +| **POST** | `/api/v1/deployments/{owner}/{deployment}/instances` | [Add a read replica to a deployment](/products/hosted/api/v1/deployment#addDeploymentInstance) | +| **DELETE** | `/api/v1/deployments/{owner}/{deployment}/instances/{id}` | [Remove an instance from a deployment](/products/hosted/api/v1/deployment#deleteDeploymentInstance) | | **GET** | `/api/v1/deployments/{owner}/{deployment}/config` | [Get a deployment's configuration](/products/hosted/api/v1/deployment#getDeploymentConfig) | | **GET** | `/api/v1/deployments/{owner}/{deployment}/backups` | [List a deployment's backups](/products/hosted/api/v1/deployment#listDeploymentBackups) | | **POST** | `/api/v1/deployments/{owner}/{deployment}/disable` | [Disable a deployment](/products/hosted/api/v1/deployment#disableDeployment) | @@ -97,6 +99,8 @@ Creating a deployment returns `202 Accepted` with the deployment in its `startin [Disabling a deployment](/products/hosted/api/v1/deployment#disableDeployment) works the same way: `202 Accepted` with the deployment in `stopping`, then poll until `state` is `stopped`. +Instance changes are also `202`, but there is no per-instance `state` field to poll, so they are observed through the [instance list](/products/hosted/api/v1/deployment#listDeploymentInstances) instead. After [adding a replica](/products/hosted/api/v1/deployment#addDeploymentInstance), poll until that instance reports a `host` — that is when it is reachable. After [removing one](/products/hosted/api/v1/deployment#deleteDeploymentInstance), poll until it disappears from the list, which only reports instances that aren't stopped. + Deployment names are unique within an owner, which makes creates idempotent by name: retrying after an ambiguous failure returns `409 Conflict` rather than provisioning a second deployment. > **Creating a deployment incurs cost.** Disabling one tears down its instances and their storage — [take a backup first](/products/hosted/api/v1/deployment#listDeploymentBackups) if you want the data. diff --git a/site/dolt/src/content/products/hosted/api/v1/deployment.md b/site/dolt/src/content/products/hosted/api/v1/deployment.md index 9ae844a..a5cc2b6 100644 --- a/site/dolt/src/content/products/hosted/api/v1/deployment.md +++ b/site/dolt/src/content/products/hosted/api/v1/deployment.md @@ -305,13 +305,6 @@ Stopped instances are not listed; starting, started, and stopping ones all are. The list is not paginated: a deployment has a primary and its replicas, a set small enough to return whole. -**Parameters** - -| Name | In | Type | Required | Description | -|------|----|------|----------|-------------| -| `owner` | path | string | yes | The user or organization that owns the deployment. 3–32 characters of letters, digits, hyphens, and underscores. | -| `deployment` | path | string | yes | The deployment name, unique within the owner. 3–32 characters of letters, digits, hyphens, and underscores. | - **Example request** ```sh @@ -362,6 +355,70 @@ curl -X GET 'https://hosted.doltdb.com/api/v1/deployments/{owner}/{deployment}/i --- +## Add a read replica to a deployment {#addDeploymentInstance} +POST /api/v1/deployments/{owner}/{deployment}/instances + +Adds an instance to `{owner}/{deployment}` and returns `202` with the new instance, which is still being provisioned and so has no `host` yet. Poll `GET /api/v1/deployments/{owner}/{deployment}/instances` until that instance reports a `host`; that is when it is reachable. There is no per-instance state field to watch. + +This is also how a disabled deployment is started again: adding an instance clears the shutdown and brings it back to `starting`. Pass `backup_id` to restore a backup into it, or it comes back empty. + +`instance_type_id` and `volume_type_id` take the **ids** from the deployment options endpoint, not the display names an instance reports on a read. + +Instances can only be added when the deployment is settled. If it is stopping, or any instance is still starting or stopping, the request conflicts with the deployment's current state and is rejected with `409`. Retry once it settles. + +As with any create, a `5xx` does not tell you whether the instance was added. List the instances to find out; a retry while the new instance is still starting is rejected with `409` rather than adding a second one. + +**Adding a replica incurs cost.** + + +**Request body** + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `instance_type_id` | string | yes | The **id** of the instance type, from the deployment options endpoint. Note an instance reports `instance_type_name` on a read — the id and the display name are different values. | +| `volume_type_id` | string | yes | The **id** of the storage type, from the deployment options endpoint. | +| `volume_size_gb` | integer | yes | The size of the instance's storage volume, in gigabytes. Must fall within the selected storage type's supported range. | +| `backup_id` | string | no | A backup of this deployment to restore into the new instance, from the backups list. Only valid when the deployment is disabled and this request is restarting it; supplying it otherwise is a `400`. Without it a restarted deployment comes up empty. | + +**Example request** + +```sh +curl -X POST 'https://hosted.doltdb.com/api/v1/deployments/{owner}/{deployment}/instances' \ + -H 'Authorization: Bearer YOUR_TOKEN' \ + -H 'Content-Type: application/json' \ + -d '{"instance_type_id":"aws.t2.medium","volume_type_id":"aws.ebs.gp3_50","volume_size_gb":50}' +``` + +**Responses** + +| Status | Description | Schema | +|--------|-------------|--------| +| `202` | The instance has been accepted and is starting. | [`DeploymentInstance`](/products/hosted/api/v1/models#model-deploymentinstance) | +| `400` | The request was malformed or failed input validation. | [`Problem`](/products/hosted/api/v1/models#model-problem) | +| `401` | Authentication credentials were missing or invalid. | [`Problem`](/products/hosted/api/v1/models#model-problem) | +| `403` | Authenticated, but not permitted to perform this action. | [`Problem`](/products/hosted/api/v1/models#model-problem) | +| `404` | The requested resource does not exist. | [`Problem`](/products/hosted/api/v1/models#model-problem) | +| `405` | The HTTP method is not supported for this resource. | [`Problem`](/products/hosted/api/v1/models#model-problem) | +| `409` | The request conflicts with the current state of the resource (e.g. it already exists). | [`Problem`](/products/hosted/api/v1/models#model-problem) | +| `500` | An unexpected server error occurred. | [`Problem`](/products/hosted/api/v1/models#model-problem) | + +**Example response `202`** + +```json +{ + "data": { + "id": "2c4e6a8b-1d3f-4a5c-8e9b-0f1a2b3c4d5e", + "index": 1, + "is_primary": false, + "instance_type_name": "t2.medium", + "volume_type_name": "Trial 50GB EBS", + "volume_size_gb": 50 + } +} +``` + +--- + ## List a deployment's backups {#listDeploymentBackups} GET /api/v1/deployments/{owner}/{deployment}/backups @@ -540,3 +597,49 @@ curl -X POST 'https://hosted.doltdb.com/api/v1/deployments/{owner}/{deployment}/ } ``` +--- + +## Remove an instance from a deployment {#deleteDeploymentInstance} +DELETE /api/v1/deployments/{owner}/{deployment}/instances/{id} + +Removes an instance from `{owner}/{deployment}` and returns `202`. The instance is marked stopping and torn down in the background. + +There is no per-instance state on this API, so completion is observed by the instance leaving `GET /api/v1/deployments/{owner}/{deployment}/instances` — that list reports only instances that have not stopped. An instance that is still present is either running or still stopping. + +Instances can only be removed when the deployment is settled. If it is stopping, or any instance is still starting or stopping, the request conflicts with the deployment's current state and is rejected with `409`. Retry once it settles. Removing an instance that has already stopped is `422`. + +**This removes a database server and the data on its volume.** It is meant for removing a read replica, so check `is_primary` on the instances list before picking an id: removing the primary shuts the deployment down. To do that, use `POST /api/v1/deployments/{owner}/{deployment}/disable`, which records `disabled_at` and `disabled_by` — removing the instance leaves the deployment with nothing running and no record of why. + + +**Example request** + +```sh +curl -X DELETE 'https://hosted.doltdb.com/api/v1/deployments/{owner}/{deployment}/instances/{id}' \ + -H 'Authorization: Bearer YOUR_TOKEN' +``` + +**Responses** + +| Status | Description | Schema | +|--------|-------------|--------| +| `202` | The removal has been accepted and the instance is stopping. | [`InstanceDeleteAccepted`](/products/hosted/api/v1/models#model-instancedeleteaccepted) | +| `400` | The request was malformed or failed input validation. | [`Problem`](/products/hosted/api/v1/models#model-problem) | +| `401` | Authentication credentials were missing or invalid. | [`Problem`](/products/hosted/api/v1/models#model-problem) | +| `403` | Authenticated, but not permitted to perform this action. | [`Problem`](/products/hosted/api/v1/models#model-problem) | +| `404` | The requested resource does not exist. | [`Problem`](/products/hosted/api/v1/models#model-problem) | +| `405` | The HTTP method is not supported for this resource. | [`Problem`](/products/hosted/api/v1/models#model-problem) | +| `409` | The request conflicts with the current state of the resource (e.g. it already exists). | [`Problem`](/products/hosted/api/v1/models#model-problem) | +| `422` | The request was well-formed but semantically invalid. | [`Problem`](/products/hosted/api/v1/models#model-problem) | +| `500` | An unexpected server error occurred. | [`Problem`](/products/hosted/api/v1/models#model-problem) | + +**Example response `202`** + +```json +{ + "data": { + "id": "2c4e6a8b-1d3f-4a5c-8e9b-0f1a2b3c4d5e", + "state": "stopping" + } +} +``` + diff --git a/site/dolt/src/content/products/hosted/api/v1/models.md b/site/dolt/src/content/products/hosted/api/v1/models.md index 6581a30..325a0f9 100644 --- a/site/dolt/src/content/products/hosted/api/v1/models.md +++ b/site/dolt/src/content/products/hosted/api/v1/models.md @@ -136,6 +136,28 @@ Settings are wrapped in an object rather than returned as a bare list so the res --- +## AddInstanceRequest {#model-addinstancerequest} +The instance to add to a deployment. + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `instance_type_id` | `string` | yes | The **id** of the instance type, from the deployment options endpoint. Note an instance reports `instance_type_name` on a read — the id and the display name are different values. | +| `volume_type_id` | `string` | yes | The **id** of the storage type, from the deployment options endpoint. | +| `volume_size_gb` | `integer` | yes | The size of the instance's storage volume, in gigabytes. Must fall within the selected storage type's supported range. | +| `backup_id` | `string` | no | A backup of this deployment to restore into the new instance, from the backups list. Only valid when the deployment is disabled and this request is restarting it; supplying it otherwise is a `400`. Without it a restarted deployment comes up empty. | + +--- + +## InstanceDeleteAccepted {#model-instancedeleteaccepted} +Acknowledges that an instance has been accepted for removal. + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `id` | `string` | yes | The instance that is being removed. | +| `state` | `string` | yes | Always `stopping`. The instance is being torn down; it leaves the instances list once that finishes. | + +--- + ## DeploymentInstance {#model-deploymentinstance} One instance backing a deployment. A deployment has a primary and, when it has read replicas, one instance per replica. @@ -144,7 +166,7 @@ One instance backing a deployment. A deployment has a primary and, when it has r | `id` | `string` | yes | The instance's identifier, unique within the deployment. | | `index` | `integer` | yes | The instance's position in the deployment. `0` is the first instance; replicas take the following indices. | | `is_primary` | `boolean` | yes | Whether this instance is currently the primary. Exactly one instance of a started deployment is primary, and which one can change over the deployment's life. | -| `host` | `string` | no | The hostname for this specific instance. Connect to the deployment's own `host` unless you mean to address one instance directly. | +| `host` | `string` | no | The hostname for this specific instance. Connect to the deployment's own `host` unless you mean to address one instance directly. Absent until the instance has come up and reported its address, so an instance that is still being provisioned has no `host`. There is no per-instance state on this API; `host` appearing is what tells you a newly added instance is reachable. | | `instance_type_name` | `string` | no | The display name of this instance's type. | | `volume_type_name` | `string` | no | The display name of this instance's storage type. | | `volume_size_gb` | `integer` | no | The size of this instance's storage volume, in gigabytes. | diff --git a/specs/hosted-v1.yaml b/specs/hosted-v1.yaml index 41945e6..116e6c5 100644 --- a/specs/hosted-v1.yaml +++ b/specs/hosted-v1.yaml @@ -503,6 +503,27 @@ paths: $ref: "#/components/responses/ServiceUnavailable" /api/v1/deployments/{owner}/{deployment}/instances: + parameters: + - name: owner + in: path + required: true + description: >- + The user or organization that owns the deployment. 3–32 characters of letters, + digits, hyphens, and underscores. + schema: + type: string + pattern: "^[-a-zA-Z0-9_]{3,32}$" + example: acme + - name: deployment + in: path + required: true + description: >- + The deployment name, unique within the owner. 3–32 characters of letters, + digits, hyphens, and underscores. + schema: + type: string + pattern: "^[-a-zA-Z0-9_]{3,32}$" + example: analytics get: operationId: listDeploymentInstances summary: List a deployment's instances. @@ -523,27 +544,6 @@ paths: - Deployment security: - apiToken: [] - parameters: - - name: owner - in: path - required: true - description: >- - The user or organization that owns the deployment. 3–32 characters of letters, - digits, hyphens, and underscores. - schema: - type: string - pattern: "^[-a-zA-Z0-9_]{3,32}$" - example: acme - - name: deployment - in: path - required: true - description: >- - The deployment name, unique within the owner. 3–32 characters of letters, - digits, hyphens, and underscores. - schema: - type: string - pattern: "^[-a-zA-Z0-9_]{3,32}$" - example: analytics responses: "200": description: The deployment's non-stopped instances. @@ -598,6 +598,97 @@ paths: "503": $ref: "#/components/responses/ServiceUnavailable" + post: + operationId: addDeploymentInstance + summary: Add a read replica to a deployment. + description: >- + Adds an instance to `{owner}/{deployment}` and returns `202` with the new instance, + which is still being provisioned and so has no `host` yet. Poll + `GET /api/v1/deployments/{owner}/{deployment}/instances` until that instance reports + a `host`; that is when it is reachable. There is no per-instance state field to + watch. + + + This is also how a disabled deployment is started again: adding an instance clears + the shutdown and brings it back to `starting`. Pass `backup_id` to restore a backup + into it, or it comes back empty. + + + `instance_type_id` and `volume_type_id` take the **ids** from the deployment options + endpoint, not the display names an instance reports on a read. + + + Instances can only be added when the deployment is settled. If it is stopping, or + any instance is still starting or stopping, the request conflicts with the + deployment's current state and is rejected with `409`. Retry once it settles. + + + As with any create, a `5xx` does not tell you whether the instance was added. List + the instances to find out; a retry while the new instance is still starting is + rejected with `409` rather than adding a second one. + + + **Adding a replica incurs cost.** + tags: + - Deployment + security: + - apiToken: [] + requestBody: + required: true + description: The instance to add. + content: + application/json: + schema: + $ref: "#/components/schemas/AddInstanceRequest" + examples: + default: + summary: A trial-tier replica. + value: + instance_type_id: aws.t2.medium + volume_type_id: aws.ebs.gp3_50 + volume_size_gb: 50 + responses: + "202": + description: The instance has been accepted and is starting. + headers: + x-request-id: + $ref: "#/components/headers/RequestId" + content: + application/json: + schema: + allOf: + - $ref: "#/components/schemas/Envelope" + - type: object + required: + - data + properties: + data: + $ref: "#/components/schemas/DeploymentInstance" + examples: + default: + summary: The replica just added. + value: + data: + id: 2c4e6a8b-1d3f-4a5c-8e9b-0f1a2b3c4d5e + index: 1 + is_primary: false + instance_type_name: t2.medium + volume_type_name: Trial 50GB EBS + volume_size_gb: 50 + "400": + $ref: "#/components/responses/BadRequest" + "401": + $ref: "#/components/responses/Unauthorized" + "403": + $ref: "#/components/responses/Forbidden" + "404": + $ref: "#/components/responses/NotFound" + "405": + $ref: "#/components/responses/MethodNotAllowed" + "409": + $ref: "#/components/responses/Conflict" + "500": + $ref: "#/components/responses/InternalServerError" /api/v1/deployments/{owner}/{deployment}/backups: get: operationId: listDeploymentBackups @@ -861,6 +952,107 @@ paths: "503": $ref: "#/components/responses/ServiceUnavailable" + /api/v1/deployments/{owner}/{deployment}/instances/{id}: + parameters: + - name: owner + in: path + required: true + description: >- + The user or organization that owns the deployment. 3–32 characters of letters, + digits, hyphens, and underscores. + schema: + type: string + pattern: "^[-a-zA-Z0-9_]{3,32}$" + example: acme + - name: deployment + in: path + required: true + description: >- + The deployment name, unique within the owner. 3–32 characters of letters, + digits, hyphens, and underscores. + schema: + type: string + pattern: "^[-a-zA-Z0-9_]{3,32}$" + example: analytics + - name: id + in: path + required: true + description: The instance's id, as reported by the instances list. + schema: + type: string + format: uuid + example: 2c4e6a8b-1d3f-4a5c-8e9b-0f1a2b3c4d5e + delete: + operationId: deleteDeploymentInstance + summary: Remove an instance from a deployment. + description: >- + Removes an instance from `{owner}/{deployment}` and returns `202`. The instance is + marked stopping and torn down in the background. + + + There is no per-instance state on this API, so completion is observed by the + instance leaving `GET /api/v1/deployments/{owner}/{deployment}/instances` — that + list reports only instances that have not stopped. An instance that is still present + is either running or still stopping. + + + Instances can only be removed when the deployment is settled. If it is stopping, or + any instance is still starting or stopping, the request conflicts with the + deployment's current state and is rejected with `409`. Retry once it settles. + Removing an instance that has already stopped is `422`. + + + **This removes a database server and the data on its volume.** It is meant for + removing a read replica, so check `is_primary` on the instances list before picking + an id: removing the primary shuts the deployment down. To do that, use + `POST /api/v1/deployments/{owner}/{deployment}/disable`, which records `disabled_at` + and `disabled_by` — removing the instance leaves the deployment with nothing running + and no record of why. + tags: + - Deployment + security: + - apiToken: [] + responses: + "202": + description: The removal has been accepted and the instance is stopping. + headers: + x-request-id: + $ref: "#/components/headers/RequestId" + content: + application/json: + schema: + allOf: + - $ref: "#/components/schemas/Envelope" + - type: object + required: + - data + properties: + data: + $ref: "#/components/schemas/InstanceDeleteAccepted" + examples: + default: + summary: The replica is stopping. + value: + data: + id: 2c4e6a8b-1d3f-4a5c-8e9b-0f1a2b3c4d5e + state: stopping + "400": + $ref: "#/components/responses/BadRequest" + "401": + $ref: "#/components/responses/Unauthorized" + "403": + $ref: "#/components/responses/Forbidden" + "404": + $ref: "#/components/responses/NotFound" + "405": + $ref: "#/components/responses/MethodNotAllowed" + "409": + $ref: "#/components/responses/Conflict" + "422": + $ref: "#/components/responses/UnprocessableEntity" + "500": + $ref: "#/components/responses/InternalServerError" + components: headers: RequestId: @@ -1227,6 +1419,73 @@ components: items: $ref: "#/components/schemas/ConfigSetting" + AddInstanceRequest: + type: object + title: AddInstanceRequest + description: >- + The instance to add to a deployment. + required: + - instance_type_id + - volume_type_id + - volume_size_gb + properties: + instance_type_id: + type: string + description: >- + The **id** of the instance type, from the deployment options endpoint. Note an + instance reports `instance_type_name` on a read — the id and the display name are + different values. + minLength: 1 + examples: + - aws.t2.medium + volume_type_id: + type: string + description: >- + The **id** of the storage type, from the deployment options endpoint. + minLength: 1 + examples: + - aws.ebs.gp3_50 + volume_size_gb: + type: integer + format: int32 + description: >- + The size of the instance's storage volume, in gigabytes. Must fall within the + selected storage type's supported range. + minimum: 1 + maximum: 2147483647 + examples: + - 50 + backup_id: + type: string + pattern: "^[0-9]{8}T[0-9]{6}\\.[0-9]{3}$" + description: >- + A backup of this deployment to restore into the new instance, from the backups + list. Only valid when the deployment is disabled and this request is restarting + it; supplying it otherwise is a `400`. Without it a restarted deployment comes + up empty. + examples: + - 20260811T020000.000 + InstanceDeleteAccepted: + type: object + title: InstanceDeleteAccepted + description: Acknowledges that an instance has been accepted for removal. + required: + - id + - state + properties: + id: + type: string + format: uuid + description: The instance that is being removed. + examples: + - 2c4e6a8b-1d3f-4a5c-8e9b-0f1a2b3c4d5e + state: + type: string + description: >- + Always `stopping`. The instance is being torn down; it leaves the instances list + once that finishes. + enum: + - stopping DeploymentInstance: type: object title: DeploymentInstance @@ -1265,6 +1524,12 @@ components: description: >- The hostname for this specific instance. Connect to the deployment's own `host` unless you mean to address one instance directly. + + + Absent until the instance has come up and reported its address, so an instance + that is still being provisioned has no `host`. There is no per-instance state on + this API; `host` appearing is what tells you a newly added instance is + reachable. examples: - analytics-0.dbs.hosted.doltdb.com instance_type_name: @@ -1398,6 +1663,7 @@ components: description: >- The size of the storage volume, in gigabytes. Must fall within the selected storage type's supported range. + maximum: 2147483647 minimum: 1 examples: - 50 @@ -1405,6 +1671,7 @@ components: type: integer format: int32 description: The number of read replicas. Defaults to `0` when omitted. + maximum: 2147483647 minimum: 0 examples: - 2 From c2a2e2d6af11f30ed66208462723234cd9fc621b Mon Sep 17 00:00:00 2001 From: Taylor Bantle Date: Thu, 20 Aug 2026 11:38:16 -0700 Subject: [PATCH 3/3] Honor path-item-level parameters in the docs generator MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Parameters table disappeared from the instance endpoints. The generator read only operation.parameters, but the spec now declares owner/deployment (and id) on the path item, which OpenAPI 3.1 §4.8.9 says every operation under that path inherits. Hoisting them there is the natural thing to do once a path has more than one method, which is what adding POST /instances did — so the same commit that introduced the new endpoints silently stripped the parameters off the existing GET. endpointBlock now merges the path item's parameters with the operation's, with operation-level entries overriding an inherited one of the same name and location, per the spec. The merged set also feeds the curl example, so a required query parameter declared on a path item still lands in the URL. Restores 2 parameters on listDeploymentInstances and documents 2 on addDeploymentInstance and 3 on deleteDeploymentInstance. Audited against the spec: all 11 hosted operations now render a Parameters table exactly when the spec defines parameters for them. DoltHub v2 declares none at the path-item level, so its output is byte-identical. Co-Authored-By: Claude Opus 5 (1M context) --- scripts/lib/openapi-docs.mjs | 48 +++++++++++++++---- .../products/hosted/api/v1/deployment.md | 22 +++++++++ 2 files changed, 60 insertions(+), 10 deletions(-) diff --git a/scripts/lib/openapi-docs.mjs b/scripts/lib/openapi-docs.mjs index e0a8cad..49bff24 100644 --- a/scripts/lib/openapi-docs.mjs +++ b/scripts/lib/openapi-docs.mjs @@ -169,8 +169,8 @@ function createRenderer(spec, { baseUrl, tokenPlaceholder, modelsHref }) { return `?${pairs.join("&")}`; } - function curlExample(method, path, operation) { - const url = `${baseUrl}${path}${requiredQueryString(operation.parameters)}`; + function curlExample(method, path, operation, params) { + const url = `${baseUrl}${path}${requiredQueryString(params ?? operation.parameters)}`; const lines = [ `curl -X ${METHOD_LABELS[method] ?? method.toUpperCase()} '${url}'`, ` -H 'Authorization: Bearer ${tokenPlaceholder}'`, @@ -311,7 +311,26 @@ function createRenderer(spec, { baseUrl, tokenPlaceholder, modelsHref }) { return `\n**Responses**\n\n| Status | Description | Schema |\n|--------|-------------|--------|\n${rows}\n`; } - function endpointBlock(method, path, operation, headingLevel = "###") { + // Parameters declared on the path item apply to every operation under it + // (OpenAPI 3.1 §4.8.9) — a spec typically hoists them there once a path has + // more than one method. Operation-level entries override an inherited one + // with the same name and location. + function effectiveParameters(pathParams, opParams) { + const merged = []; + const seen = new Map(); // "in:name" -> index in merged + for (const raw of [...(pathParams ?? []), ...(opParams ?? [])]) { + const p = deref(raw); + const key = `${p.in}:${p.name}`; + if (seen.has(key)) merged[seen.get(key)] = p; + else { + seen.set(key, merged.length); + merged.push(p); + } + } + return merged; + } + + function endpointBlock(method, path, operation, headingLevel = "###", pathParams) { const anchor = `{#${operation.operationId}}`; const title = operation.summary ? operation.summary.replace(/\.$/, "") @@ -323,11 +342,12 @@ function createRenderer(spec, { baseUrl, tokenPlaceholder, modelsHref }) { operation.description.trim() !== (operation.summary ?? "").trim() ? `${operation.description.trim()}\n\n` : ""; - const params = parametersSection(operation.parameters); + const allParams = effectiveParameters(pathParams, operation.parameters); + const params = parametersSection(allParams); const body = requestBodySection(deref(operation.requestBody ?? {})); const responses = responsesSection(operation.responses); const successExample = successExampleBlock(operation.responses); - const curl = `\n**Example request**\n\n\`\`\`sh\n${curlExample(method, path, operation)}\n\`\`\`\n`; + const curl = `\n**Example request**\n\n\`\`\`sh\n${curlExample(method, path, operation, allParams)}\n\`\`\`\n`; return [ heading, methodPath, @@ -426,14 +446,20 @@ export function generateApiDocs(config) { modelsHref: models.href, }); - // Collect operations by tag. - const byTag = {}; // tag → [{ method, path, operation }] + // Collect operations by tag. pathParams carries the path item's own + // parameters, which every operation under it inherits. + const byTag = {}; // tag → [{ method, path, operation, pathParams }] for (const [path, pathItem] of Object.entries(spec.paths ?? {})) { for (const method of HTTP_METHODS) { const op = pathItem[method]; if (!op) continue; for (const tag of op.tags ?? ["Untagged"]) { - (byTag[tag] ??= []).push({ method, path, operation: op }); + (byTag[tag] ??= []).push({ + method, + path, + operation: op, + pathParams: pathItem.parameters, + }); } } } @@ -457,7 +483,7 @@ export function generateApiDocs(config) { .map((g) => { const endpoints = grouped[g] .map((item) => - endpointBlock(item.method, item.path, item.operation, "###") + endpointBlock(item.method, item.path, item.operation, "###", item.pathParams) ) .join("\n---\n\n"); return `## ${g}\n\n${endpoints}`; @@ -467,7 +493,9 @@ export function generateApiDocs(config) { } const content = items - .map((item) => endpointBlock(item.method, item.path, item.operation, "##")) + .map((item) => + endpointBlock(item.method, item.path, item.operation, "##", item.pathParams) + ) .join("\n---\n\n"); return `${frontmatter}\n\n${intro}${content}\n`; } diff --git a/site/dolt/src/content/products/hosted/api/v1/deployment.md b/site/dolt/src/content/products/hosted/api/v1/deployment.md index a5cc2b6..961f128 100644 --- a/site/dolt/src/content/products/hosted/api/v1/deployment.md +++ b/site/dolt/src/content/products/hosted/api/v1/deployment.md @@ -305,6 +305,13 @@ Stopped instances are not listed; starting, started, and stopping ones all are. The list is not paginated: a deployment has a primary and its replicas, a set small enough to return whole. +**Parameters** + +| Name | In | Type | Required | Description | +|------|----|------|----------|-------------| +| `owner` | path | string | yes | The user or organization that owns the deployment. 3–32 characters of letters, digits, hyphens, and underscores. | +| `deployment` | path | string | yes | The deployment name, unique within the owner. 3–32 characters of letters, digits, hyphens, and underscores. | + **Example request** ```sh @@ -371,6 +378,13 @@ As with any create, a `5xx` does not tell you whether the instance was added. Li **Adding a replica incurs cost.** +**Parameters** + +| Name | In | Type | Required | Description | +|------|----|------|----------|-------------| +| `owner` | path | string | yes | The user or organization that owns the deployment. 3–32 characters of letters, digits, hyphens, and underscores. | +| `deployment` | path | string | yes | The deployment name, unique within the owner. 3–32 characters of letters, digits, hyphens, and underscores. | + **Request body** | Field | Type | Required | Description | @@ -611,6 +625,14 @@ Instances can only be removed when the deployment is settled. If it is stopping, **This removes a database server and the data on its volume.** It is meant for removing a read replica, so check `is_primary` on the instances list before picking an id: removing the primary shuts the deployment down. To do that, use `POST /api/v1/deployments/{owner}/{deployment}/disable`, which records `disabled_at` and `disabled_by` — removing the instance leaves the deployment with nothing running and no record of why. +**Parameters** + +| Name | In | Type | Required | Description | +|------|----|------|----------|-------------| +| `owner` | path | string | yes | The user or organization that owns the deployment. 3–32 characters of letters, digits, hyphens, and underscores. | +| `deployment` | path | string | yes | The deployment name, unique within the owner. 3–32 characters of letters, digits, hyphens, and underscores. | +| `id` | path | string | yes | The instance's id, as reported by the instances list. | + **Example request** ```sh