@selfhost.dev/mcp-server 0.9.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +205 -43
- package/dist/client.d.ts +7 -0
- package/dist/client.js +2 -1
- package/dist/client.js.map +1 -1
- package/dist/index.js +21 -1
- package/dist/index.js.map +1 -1
- package/dist/tools/alerts.js +12 -2
- package/dist/tools/alerts.js.map +1 -1
- package/dist/tools/backups.js +29 -5
- package/dist/tools/backups.js.map +1 -1
- package/dist/tools/billing.js +233 -39
- package/dist/tools/billing.js.map +1 -1
- package/dist/tools/database-users.js +53 -6
- package/dist/tools/database-users.js.map +1 -1
- package/dist/tools/deployments.js +47 -3
- package/dist/tools/deployments.js.map +1 -1
- package/dist/tools/github.js +21 -4
- package/dist/tools/github.js.map +1 -1
- package/dist/tools/instance-logs.js +2 -2
- package/dist/tools/instance-logs.js.map +1 -1
- package/dist/tools/instances.js +423 -88
- package/dist/tools/instances.js.map +1 -1
- package/dist/tools/organizations.js +5 -1
- package/dist/tools/organizations.js.map +1 -1
- package/dist/tools/pitr.d.ts +22 -0
- package/dist/tools/pitr.js +75 -10
- package/dist/tools/pitr.js.map +1 -1
- package/dist/tools/postgres-extensions.js +176 -57
- package/dist/tools/postgres-extensions.js.map +1 -1
- package/dist/tools/project-backups.js +130 -0
- package/dist/tools/project-backups.js.map +1 -1
- package/dist/tools/project-databases.js +16 -23
- package/dist/tools/project-databases.js.map +1 -1
- package/dist/tools/project-services.js +544 -53
- package/dist/tools/project-services.js.map +1 -1
- package/dist/tools/project-ssh.d.ts +2 -0
- package/dist/tools/project-ssh.js +338 -0
- package/dist/tools/project-ssh.js.map +1 -0
- package/dist/tools/projects.js +21 -3
- package/dist/tools/projects.js.map +1 -1
- package/dist/tools/scaling.js +1 -1
- package/dist/tools/scaling.js.map +1 -1
- package/dist/types/tiers.js +27 -4
- package/dist/types/tiers.js.map +1 -1
- package/package.json +2 -1
package/dist/tools/instances.js
CHANGED
|
@@ -37,48 +37,54 @@ function validateStorageParams(type, iops, throughput) {
|
|
|
37
37
|
return null;
|
|
38
38
|
}
|
|
39
39
|
// --- Engine reference data (mirrors the backend adapter registry + the console) ---
|
|
40
|
-
const ENGINES = ["postgres", "mysql", "redis", "clickhouse"];
|
|
40
|
+
const ENGINES = ["postgres", "mysql", "redis", "clickhouse", "opensearch"];
|
|
41
41
|
/**
|
|
42
|
-
* ClickHouse versions
|
|
43
|
-
*
|
|
42
|
+
* ClickHouse versions (`ClickhouseAdapter.supported_versions`). One rung: selfhost#2580 pinned
|
|
43
|
+
* the platform to the 26.8 LTS and deleted 26.5 once no live instance was left on it. The list
|
|
44
|
+
* stays an array because the next LTS lands here, not because the caller gets a choice today.
|
|
44
45
|
*/
|
|
45
|
-
const CLICKHOUSE_VERSIONS = ["
|
|
46
|
+
const CLICKHOUSE_VERSIONS = ["26.8"];
|
|
46
47
|
/** Versions each engine's adapter accepts. Anything else is a 422 at create. */
|
|
47
48
|
const ENGINE_VERSIONS = {
|
|
48
49
|
postgres: ["18", "17", "16"],
|
|
49
50
|
mysql: ["8.4", "8.0"],
|
|
50
51
|
redis: ["7.2", "7.0"],
|
|
51
52
|
clickhouse: CLICKHOUSE_VERSIONS,
|
|
53
|
+
opensearch: ["2.19", "2.16", "2.13"],
|
|
52
54
|
};
|
|
53
55
|
/**
|
|
54
56
|
* Version sent when the caller passes none.
|
|
55
57
|
*
|
|
56
58
|
* This has to be explicit on every engine: the backend's fallback is
|
|
57
59
|
* `supported_versions.first`, which is the OLDEST major each adapter still accepts
|
|
58
|
-
* (postgres 14, redis 7.0,
|
|
60
|
+
* (postgres 14, redis 7.0, opensearch 2.13), so omitting `db_version` silently
|
|
59
61
|
* provisions a years-old release. These mirror the console's create-wizard defaults
|
|
60
62
|
* (`CreateDatabaseOptions`) so both surfaces hand out the same database - newest for
|
|
61
|
-
* postgres/redis, and the stable release rather than the newest for mysql
|
|
62
|
-
*
|
|
63
|
+
* postgres/redis/opensearch, and the stable release rather than the newest for mysql
|
|
64
|
+
* (8.0 over 8.4). ClickHouse has exactly one supported version, so its default is the list.
|
|
63
65
|
*/
|
|
64
66
|
const ENGINE_DEFAULT_VERSION = {
|
|
65
67
|
postgres: "18",
|
|
66
68
|
mysql: "8.0",
|
|
67
69
|
redis: "7.2",
|
|
68
|
-
clickhouse: "
|
|
70
|
+
clickhouse: "26.8",
|
|
71
|
+
opensearch: "2.19",
|
|
69
72
|
};
|
|
70
73
|
const CLICKHOUSE_DEFAULT_VERSION = ENGINE_DEFAULT_VERSION.clickhouse;
|
|
71
74
|
/**
|
|
72
|
-
* Hard RAM floor per engine
|
|
73
|
-
* 4 GiB fails its systemd Type=notify startup
|
|
75
|
+
* Hard RAM floor per engine, from the backend's capacity-ladder YAML (`RAM_TIERS`). ClickHouse
|
|
76
|
+
* below 4 GiB fails its systemd Type=notify startup; OpenSearch gives its JVM heap 50% of
|
|
77
|
+
* available memory, so below 4 GiB it cannot hold a usable heap. Create is rejected outright
|
|
78
|
+
* on both. 512 MiB and 1 GiB remain postgres/mysql-only territory and both are `not_supported`
|
|
79
|
+
* for these two at every tier.
|
|
74
80
|
*/
|
|
75
|
-
const ENGINE_MIN_RAM_GIB = { clickhouse: 4 };
|
|
81
|
+
const ENGINE_MIN_RAM_GIB = { clickhouse: 4, opensearch: 4 };
|
|
76
82
|
/** Highest replica count offered with multi-AZ on. ClickHouse scales out; others keep Basic(1)/Enhanced(2). */
|
|
77
|
-
const ENGINE_MAX_REPLICAS = { postgres: 2, mysql: 2, redis: 2, clickhouse: 5 };
|
|
83
|
+
const ENGINE_MAX_REPLICAS = { postgres: 2, mysql: 2, redis: 2, clickhouse: 5, opensearch: 5 };
|
|
78
84
|
/** Replica count used when multi_az is on and none was passed (ClickHouse wants an odd node count). */
|
|
79
|
-
const ENGINE_DEFAULT_REPLICAS = { postgres: 1, mysql: 1, redis: 1, clickhouse: 2 };
|
|
85
|
+
const ENGINE_DEFAULT_REPLICAS = { postgres: 1, mysql: 1, redis: 1, clickhouse: 2, opensearch: 1 };
|
|
80
86
|
/** Default client port per engine. ClickHouse speaks HTTP on 8123; native TCP is 9000. */
|
|
81
|
-
const ENGINE_DEFAULT_PORT = { postgres: 5432, mysql: 3306, redis: 6379, clickhouse: 8123 };
|
|
87
|
+
const ENGINE_DEFAULT_PORT = { postgres: 5432, mysql: 3306, redis: 6379, clickhouse: 8123, opensearch: 9200 };
|
|
82
88
|
/** ClickHouse native TCP port, alongside the HTTP port above. */
|
|
83
89
|
const CLICKHOUSE_NATIVE_PORT = 9000;
|
|
84
90
|
/** Distributed-query cluster name on multi-AZ ClickHouse, e.g. cluster('selfhost_cluster', system.one). */
|
|
@@ -96,6 +102,65 @@ const CLICKHOUSE_KEEPER_STORAGE_GB = 20;
|
|
|
96
102
|
function needsClickhouseKeeper(engine, replicaCount) {
|
|
97
103
|
return engine === "clickhouse" && replicaCount === 1;
|
|
98
104
|
}
|
|
105
|
+
/**
|
|
106
|
+
* OpenSearch cluster topology - `OpensearchTopology`. OpenSearch has no master/replica
|
|
107
|
+
* relationship; a cluster is a set of nodes carrying roles, and `replica_count` buys DATA
|
|
108
|
+
* nodes. The moment redundancy is asked for, cluster-manager duties move onto three dedicated
|
|
109
|
+
* nodes, because folding the manager role onto two all-in-one nodes leaves an even,
|
|
110
|
+
* split-brain-prone manager set:
|
|
111
|
+
*
|
|
112
|
+
* replica_count 0 → 1 node (one all-in-one [cluster_manager,data,ingest]; dev / non-HA)
|
|
113
|
+
* replica_count 1 → 5 nodes (3 manager-only + 2 data) ← HA minimum
|
|
114
|
+
* replica_count 2 → 6 nodes (3 manager-only + 3 data)
|
|
115
|
+
* replica_count N → N+4 (3 manager-only + N+1 data)
|
|
116
|
+
*
|
|
117
|
+
* THE THREE MANAGERS BILL LIKE ANY OTHER NODE, and the pricing endpoint knows nothing about
|
|
118
|
+
* them - the same trap as the ClickHouse Keeper, three times over. Going from 0 to 1 replica
|
|
119
|
+
* is a 1 → 5 node jump, not 1 → 2. Managers inherit the endpoint node's storage size and
|
|
120
|
+
* default to `t4g.medium`; `manager_instance_type` overrides that, subject to the same 4 GiB
|
|
121
|
+
* floor (a 2 GiB manager OOMs its JVM under quorum load and is rejected).
|
|
122
|
+
*/
|
|
123
|
+
const OPENSEARCH_MANAGER_COUNT = 3;
|
|
124
|
+
const OPENSEARCH_DEFAULT_MANAGER_TYPE = "t4g.medium";
|
|
125
|
+
const OPENSEARCH_MANAGER_RAM_FLOOR_GIB = 4;
|
|
126
|
+
/** Total nodes provisioned for an OpenSearch `replica_count`, managers included. */
|
|
127
|
+
function opensearchNodeCount(replicaCount) {
|
|
128
|
+
const dataNodes = Math.max(replicaCount, 0) + 1;
|
|
129
|
+
return dataNodes >= 2 ? dataNodes + OPENSEARCH_MANAGER_COUNT : dataNodes;
|
|
130
|
+
}
|
|
131
|
+
/** Whether this topology provisions the three dedicated cluster-manager nodes. */
|
|
132
|
+
function needsOpensearchManagers(engine, replicaCount) {
|
|
133
|
+
return engine === "opensearch" && Math.max(replicaCount, 0) + 1 >= 2;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* OpenSearch Dashboards - the opt-in UI, co-located on the endpoint node as a Node.js process
|
|
137
|
+
* competing with the JVM for memory. Hence its own RAM gate, above the engine's own floor.
|
|
138
|
+
* Never part of provisioning or readiness: the instance is ready when 9200 is, whatever
|
|
139
|
+
* Dashboards is doing.
|
|
140
|
+
*/
|
|
141
|
+
/**
|
|
142
|
+
* PostgreSQL HA durability (selfhost#2582) - how long a COMMIT waits for a standby to flush
|
|
143
|
+
* the WAL. Three modes, and the difference between them is what happens when no standby is
|
|
144
|
+
* healthy:
|
|
145
|
+
*
|
|
146
|
+
* - `async` - the primary acks locally. Fastest, and RPO > 0: a commit can be lost if the
|
|
147
|
+
* primary dies before a standby has it.
|
|
148
|
+
* - `preferred`- waits for healthy standbys, and DEGRADES to no-wait when none are healthy. It
|
|
149
|
+
* never blocks writes, which is also why it cannot promise zero data loss.
|
|
150
|
+
* - `required` - RPO 0, and the honest version of that promise: dead standby names stay in the
|
|
151
|
+
* wait set, so when the SLA cannot be held THE PRIMARY STOPS ACCEPTING WRITES
|
|
152
|
+
* rather than quietly acking data no survivor holds. Only `required` while
|
|
153
|
+
* `sla_held` is true may be described to a user as zero-data-loss.
|
|
154
|
+
*
|
|
155
|
+
* `sync_number` is how many standbys must ack (default 1) and cannot exceed the group's HA
|
|
156
|
+
* standby count. A 2-node `required` cluster has NO automated failover - promoting the single
|
|
157
|
+
* standby would be the thing that loses data - so it needs `no_auto_failover_ack`.
|
|
158
|
+
*/
|
|
159
|
+
const PG_DURABILITY_MODES = ["async", "preferred", "required"];
|
|
160
|
+
const PG_DURABILITY_DEFAULT_SYNC_NUMBER = 1;
|
|
161
|
+
const OPENSEARCH_DASHBOARDS_PORT = 5601;
|
|
162
|
+
const DASHBOARDS_MIN_RAM_GIB = 4;
|
|
163
|
+
const DASHBOARDS_RECOMMENDED_RAM_GIB = 8;
|
|
99
164
|
/**
|
|
100
165
|
* RAM of an instance type, from the same reference-data endpoint `get_instance_types_for_region`
|
|
101
166
|
* serves (rate-limit exempt). Returns null when the type or the lookup is unavailable - callers
|
|
@@ -285,10 +350,13 @@ platform margin is folded into each line rather than billed separately, matching
|
|
|
285
350
|
console and the website - so quote these numbers to the user as-is and never add a
|
|
286
351
|
platform fee on top.
|
|
287
352
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
353
|
+
PASS type_of_dbms WHENEVER YOU KNOW THE ENGINE. Two engines provision nodes the pricing
|
|
354
|
+
endpoint cannot see, and this tool only adds them when it knows the engine:
|
|
355
|
+
- clickhouse with replica_count=1 gets a dedicated keeper-only node (${CLICKHOUSE_KEEPER_INSTANCE_TYPE} + ${CLICKHOUSE_KEEPER_STORAGE_GB} GB)
|
|
356
|
+
holding the Raft quorum. At 2+ replicas the quorum is embedded and there is no extra node.
|
|
357
|
+
- opensearch with replica_count >= 1 gets ${OPENSEARCH_MANAGER_COUNT} dedicated cluster-manager nodes on top of the
|
|
358
|
+
data nodes, each inheriting the data nodes' storage size. replica_count=1 is ${opensearchNodeCount(1)} machines,
|
|
359
|
+
not 2, so leaving the engine out understates that cluster by three whole nodes.
|
|
292
360
|
|
|
293
361
|
CLICKHOUSE ESTIMATES RUN LOW EVEN WITH THE ENGINE PASSED, and the keeper node is not the
|
|
294
362
|
reason. The estimate endpoint applies ONE flat platform margin to every engine, while
|
|
@@ -308,7 +376,7 @@ different margin entirely. Use \`list_server_types\`, which returns real per-loc
|
|
|
308
376
|
public_ipv4_count: z.number().int().min(0).optional().describe("Number of public IPv4 addresses (default: 0)"),
|
|
309
377
|
iops: z.number().optional().describe("Provisioned IOPS (for gp3/io1/io2). Defaults to storage type baseline if omitted."),
|
|
310
378
|
throughput_mbps: z.number().optional().describe("Throughput in MB/s (for gp3). Defaults to storage type baseline if omitted."),
|
|
311
|
-
type_of_dbms: z.enum(ENGINES).optional().describe("Engine being priced.
|
|
379
|
+
type_of_dbms: z.enum(ENGINES).optional().describe("Engine being priced. PASS IT FOR CLICKHOUSE AND OPENSEARCH OR THE ESTIMATE IS WRONG: clickhouse at replica_count=1 adds a billed Keeper node, and opensearch at replica_count>=1 adds 3 billed cluster-manager nodes. Both are invisible to the pricing endpoint."),
|
|
312
380
|
}, async ({ region, instance_type, storage_type, storage_size_gb, database_count, replica_count, public_ipv4_count, iops, throughput_mbps, type_of_dbms }) => {
|
|
313
381
|
const estimate = (overrides) => {
|
|
314
382
|
const body = {
|
|
@@ -341,6 +409,25 @@ different margin entirely. Use \`list_server_types\`, which returns real per-loc
|
|
|
341
409
|
// The pricing endpoint is engine-agnostic, so the standalone ClickHouse Keeper node has
|
|
342
410
|
// to be priced as its own unit and folded in: a t4g.small with a 20 GB volume that
|
|
343
411
|
// inherits the primary's storage settings, and no public IPv4 of its own.
|
|
412
|
+
let managers = null;
|
|
413
|
+
let managerError = null;
|
|
414
|
+
if (needsOpensearchManagers(type_of_dbms ?? "", replica_count ?? 0)) {
|
|
415
|
+
// Managers inherit the data nodes' storage and carry no public IPv4 of their own.
|
|
416
|
+
// Priced as ONE node here and multiplied out below, so the per-node figure stays
|
|
417
|
+
// readable in the breakdown.
|
|
418
|
+
const managerResult = await estimate({
|
|
419
|
+
instance_type: OPENSEARCH_DEFAULT_MANAGER_TYPE,
|
|
420
|
+
database_count: 1,
|
|
421
|
+
replica_count: 0,
|
|
422
|
+
public_ipv4_count: 0,
|
|
423
|
+
});
|
|
424
|
+
if (managerResult.success) {
|
|
425
|
+
managers = managerResult.data;
|
|
426
|
+
}
|
|
427
|
+
else {
|
|
428
|
+
managerError = `Could not price the ${OPENSEARCH_MANAGER_COUNT} cluster-manager nodes (${managerResult.statusCode}): ${managerResult.message}`;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
344
431
|
let keeper = null;
|
|
345
432
|
let keeperError = null;
|
|
346
433
|
if (needsClickhouseKeeper(type_of_dbms ?? "", replica_count ?? 0)) {
|
|
@@ -378,14 +465,25 @@ different margin entirely. Use \`list_server_types\`, which returns real per-loc
|
|
|
378
465
|
const keeperAllIn = foldMarkup(keeper);
|
|
379
466
|
lines.push("", `ClickHouse Keeper node (${CLICKHOUSE_KEEPER_INSTANCE_TYPE} + ${CLICKHOUSE_KEEPER_STORAGE_GB} GiB ${keeper.storage_type}):`, ` Compute: $${keeperAllIn.compute.toFixed(2)}`, ` Storage: $${keeperAllIn.storage.toFixed(2)}`, ` Subtotal: $${keeper.total_monthly.toFixed(2)}`, " (Required third quorum member because replica_count=1. Two replicas instead run the", " quorum embedded on the data nodes - no Keeper node and no third line item.)");
|
|
380
467
|
}
|
|
381
|
-
const
|
|
468
|
+
const managersMonthly = managers ? managers.total_monthly * OPENSEARCH_MANAGER_COUNT : 0;
|
|
469
|
+
if (managers) {
|
|
470
|
+
const managerAllIn = foldMarkup(managers);
|
|
471
|
+
lines.push("", `OpenSearch cluster managers (${OPENSEARCH_MANAGER_COUNT} x ${OPENSEARCH_DEFAULT_MANAGER_TYPE} + ${managers.storage_size_gb} GiB ${managers.storage_type}):`, ` Per node: compute $${managerAllIn.compute.toFixed(2)} + storage $${managerAllIn.storage.toFixed(2)} = $${managers.total_monthly.toFixed(2)}`, ` Subtotal: $${managersMonthly.toFixed(2)} (${OPENSEARCH_MANAGER_COUNT} nodes)`, ` (A quorum needs an odd number of managers, so HA starts at ${OPENSEARCH_MANAGER_COUNT} dedicated nodes rather than`, " folding the role onto the data nodes. They carry no search load but bill in full.", ` Priced at the ${OPENSEARCH_DEFAULT_MANAGER_TYPE} default - pass manager_instance_type to create_instance to change it.)`);
|
|
472
|
+
}
|
|
473
|
+
const total = data.total_monthly + (keeper?.total_monthly ?? 0) + managersMonthly;
|
|
382
474
|
lines.push("", `TOTAL: $${total.toFixed(2)}/month`);
|
|
383
475
|
if (keeper) {
|
|
384
476
|
lines.push(` (${data.replica_count + 1} data nodes $${data.total_monthly.toFixed(2)} + Keeper $${keeper.total_monthly.toFixed(2)})`);
|
|
385
477
|
}
|
|
478
|
+
if (managers) {
|
|
479
|
+
lines.push(` (${data.replica_count + 1} data nodes $${data.total_monthly.toFixed(2)} + ${OPENSEARCH_MANAGER_COUNT} managers $${managersMonthly.toFixed(2)} = ${opensearchNodeCount(replica_count ?? 0)} nodes total)`);
|
|
480
|
+
}
|
|
386
481
|
if (keeperError) {
|
|
387
482
|
lines.push("", `⚠️ ${keeperError}`, "This total EXCLUDES the Keeper node that a 1-replica ClickHouse cluster requires - the real bill will be higher.");
|
|
388
483
|
}
|
|
484
|
+
if (managerError) {
|
|
485
|
+
lines.push("", `⚠️ ${managerError}`, `This total EXCLUDES the ${OPENSEARCH_MANAGER_COUNT} cluster-manager nodes an HA OpenSearch cluster requires - the real bill will be materially higher.`);
|
|
486
|
+
}
|
|
389
487
|
lines.push("", `Pricing model: ${data.pricing_model}`, `Currency: ${data.currency}`, "All-in: every line already includes the platform margin. Charged hourly against wallet credit.");
|
|
390
488
|
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
391
489
|
});
|
|
@@ -423,45 +521,94 @@ CLICKHOUSE SPECIFICS (they differ from the other engines - read before creating
|
|
|
423
521
|
- Connect over HTTP on ${ENGINE_DEFAULT_PORT.clickhouse} (native TCP on ${CLICKHOUSE_NATIVE_PORT}) as user \`admin\`. Multi-AZ clusters
|
|
424
522
|
expose the distributed cluster \`${CLICKHOUSE_CLUSTER_NAME}\`.
|
|
425
523
|
- Use ${ENGINE_DEFAULT_PORT.clickhouse} as the port on allowed_cidr_ranges entries, not 5432/3306.
|
|
426
|
-
-
|
|
524
|
+
- Version: ${CLICKHOUSE_DEFAULT_VERSION} LTS, the only one the platform supports. It is pinned at create and
|
|
525
|
+
CANNOT be changed later - moving versions means a new instance plus a restore or migration into it.
|
|
526
|
+
|
|
527
|
+
OPENSEARCH SPECIFICS (a search/analytics engine, not a relational database - read before creating one):
|
|
528
|
+
- Minimum 4 GiB RAM: the JVM heap takes 50% of available memory. Find eligible sizes with
|
|
529
|
+
get_instance_types_for_region using min_memory=4.
|
|
530
|
+
- HA IS A CLUSTER TOPOLOGY, AND \`replica_count\` BUYS FAR MORE MACHINES THAN IT LOOKS:
|
|
531
|
+
replica_count=0 → 1 node (one all-in-one node; dev / non-HA only)
|
|
532
|
+
replica_count=1 → ${opensearchNodeCount(1)} NODES: 3 dedicated cluster-manager nodes + 2 data nodes ← HA minimum
|
|
533
|
+
replica_count=2 → ${opensearchNodeCount(2)} nodes: 3 managers + 3 data
|
|
534
|
+
replica_count=N → N+4 nodes
|
|
535
|
+
Going from no HA to HA is a 1 → ${opensearchNodeCount(1)} node jump, not 1 → 2, because a quorum needs an odd
|
|
536
|
+
number of managers and two would be split-brain-prone. ALL of those nodes bill. Price it
|
|
537
|
+
with estimate_instance_cost using type_of_dbms="opensearch" and the real replica_count, and
|
|
538
|
+
quote that number before creating - this is the single most expensive surprise on the
|
|
539
|
+
platform if you skip it.
|
|
540
|
+
- multi_az REQUIRES replica_count >= 1, and unlike every other engine it CANNOT BE TOGGLED
|
|
541
|
+
LATER in either direction. Enabling afterwards would build a 2-manager set with 0-replica
|
|
542
|
+
indices; disabling would terminate nodes with no shard drain. Decide HA at create or plan on
|
|
543
|
+
creating a new cluster and restoring into it.
|
|
544
|
+
- manager_instance_type sets the three managers' size (default ${OPENSEARCH_DEFAULT_MANAGER_TYPE}). Same ${OPENSEARCH_MANAGER_RAM_FLOOR_GIB} GiB floor -
|
|
545
|
+
managers carry no indexing or search load, so they can be smaller than the data nodes, but
|
|
546
|
+
not smaller than that. They inherit the endpoint node's storage size.
|
|
547
|
+
- ALWAYS HTTPS ON ${ENGINE_DEFAULT_PORT.opensearch}, never plaintext. Use ${ENGINE_DEFAULT_PORT.opensearch} on allowed_cidr_ranges entries. Transport
|
|
548
|
+
port 9300 is node-to-node only and is never customer-facing.
|
|
549
|
+
- TLS CAVEAT WORTH STATING UP FRONT: certs are per-cluster and CA-signed, and the node cert's
|
|
550
|
+
SAN covers the private IP rather than the public DNS name - so strict hostname verification
|
|
551
|
+
against the endpoint fails even with the CA trusted. Tell users to trust the CA with relaxed
|
|
552
|
+
hostname checking, or connect from inside the VPC. \`curl -k\` for a quick start. The cluster
|
|
553
|
+
CA is not downloadable through the API yet.
|
|
554
|
+
- No named databases (like redis) - do not ask for or display a database name.
|
|
555
|
+
- Vector search is BUILT IN (the k-NN plugin ships with the engine), so an embeddings workload
|
|
556
|
+
needs no extension here - that is a postgres/pgvector concern.
|
|
557
|
+
- dashboards_enabled turns on the OpenSearch Dashboards UI on port ${OPENSEARCH_DASHBOARDS_PORT}. It needs ${DASHBOARDS_MIN_RAM_GIB} GiB minimum
|
|
558
|
+
(${DASHBOARDS_RECOMMENDED_RAM_GIB} GiB recommended) because it runs alongside the JVM on the endpoint node, and it can be
|
|
559
|
+
turned on or off later with set_opensearch_dashboards. It never blocks readiness.
|
|
560
|
+
- Version: ${ENGINE_DEFAULT_VERSION.opensearch} is sent by default. Do not omit db_version - the backend adapter has no
|
|
561
|
+
default override, so an omitted field resolves to ${ENGINE_VERSIONS.opensearch[ENGINE_VERSIONS.opensearch.length - 1]}, the oldest.
|
|
427
562
|
|
|
428
563
|
ENGINE-SPECIFIC FOLLOW-UPS (route to the right tools per engine after creating):
|
|
429
|
-
- Connection pooling: PgBouncer for postgres, ProxySQL for mysql, CHProxy for clickhouse, none for redis.
|
|
430
|
-
- Performance tuning: the pg-config tools for postgres, the mysql-config tools for mysql; none for redis or
|
|
431
|
-
- PITR applies to postgres and mysql only - not redis, not clickhouse
|
|
432
|
-
-
|
|
433
|
-
-
|
|
434
|
-
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
564
|
+
- Connection pooling: PgBouncer for postgres, ProxySQL for mysql, CHProxy for clickhouse, none for redis or opensearch.
|
|
565
|
+
- Performance tuning: the pg-config tools for postgres, the mysql-config tools for mysql; none for redis, clickhouse or opensearch.
|
|
566
|
+
- PITR applies to postgres and mysql only - not redis, not clickhouse, not opensearch (there is no
|
|
567
|
+
WAL-G equivalent, so opensearch durability is EBS volume snapshots and whole-volume restores only).
|
|
568
|
+
- database-users applies to postgres, mysql, clickhouse and opensearch - not redis.
|
|
569
|
+
- postgresql_config and \`extensions\` are postgres-only.
|
|
570
|
+
- db_version is fixed for the life of the instance on every engine. There is no in-place version
|
|
571
|
+
upgrade anywhere on the platform any more (selfhost#2580 withdrew the ClickHouse one).
|
|
572
|
+
|
|
573
|
+
POSTGRES EXTENSIONS - pass \`extensions: [...]\` (an ARRAY of catalog names, not booleans):
|
|
574
|
+
- TimescaleDB is \`timescaledb\`, pgvector is \`pgvector\` (or its SQL alias \`vector\`). The old
|
|
575
|
+
\`timescaledb_enabled\` / \`vector_enabled\` flags were REMOVED backend-side and are now
|
|
576
|
+
IGNORED rather than rejected - sending them provisions an instance with no extensions at all.
|
|
577
|
+
- Call \`list_postgres_extension_catalog\` with the \`db_version\` you are about to use to see
|
|
578
|
+
what is on offer; compatibility is per entry (\`pg_textsearch\` needs PostgreSQL 17 or 18).
|
|
579
|
+
Do not work from a memorised list.
|
|
580
|
+
- Extensions chosen here are installed during provisioning, so the ones that need a PostgreSQL
|
|
581
|
+
restart cost nothing extra at create - there is no restart to warn about on this path.
|
|
582
|
+
- Readiness accounts for them: the instance is not marked ready while any requested extension
|
|
583
|
+
is missing. Any of them can be added later with \`enable_instance_extensions\`, but NONE can
|
|
584
|
+
ever be removed, so ask the user before adding to the list.`, {
|
|
443
585
|
organization_id: z.string().optional().describe("Organization PID (defaults to active org)"),
|
|
444
586
|
region: z.string().min(1, "AWS region is required (e.g. us-east-1)"),
|
|
445
587
|
identifier: z.string().min(1, "Database instance name"),
|
|
446
588
|
instance_type: z.string().min(1, "EC2 instance type (e.g. t3.micro, r6i.xlarge)"),
|
|
447
589
|
type_of_dbms: z.enum(ENGINES).describe("Database engine"),
|
|
448
590
|
storage: storageSchema,
|
|
449
|
-
db_version: z.string().optional().describe(`Database version. postgres: ${ENGINE_VERSIONS.postgres.join(", ")} (default ${ENGINE_DEFAULT_VERSION.postgres}). mysql: ${ENGINE_VERSIONS.mysql.join(", ")} (default ${ENGINE_DEFAULT_VERSION.mysql}, the stable release). redis: ${ENGINE_VERSIONS.redis.join(", ")} (default ${ENGINE_DEFAULT_VERSION.redis}). clickhouse: ${ENGINE_VERSIONS.clickhouse.join(", ")} (default ${ENGINE_DEFAULT_VERSION.clickhouse}, the LTS). Omitting this sends the default explicitly - the backend's own fallback is the oldest major it still supports.`),
|
|
450
|
-
username: z.string().regex(/^[a-z_][a-z0-9_-]{0,30}$/i).optional().describe("Database username (default: admin). Redis ignores this (auth is a single requirepass/password); it applies to postgres, mysql and
|
|
591
|
+
db_version: z.string().optional().describe(`Database version. postgres: ${ENGINE_VERSIONS.postgres.join(", ")} (default ${ENGINE_DEFAULT_VERSION.postgres}). mysql: ${ENGINE_VERSIONS.mysql.join(", ")} (default ${ENGINE_DEFAULT_VERSION.mysql}, the stable release). redis: ${ENGINE_VERSIONS.redis.join(", ")} (default ${ENGINE_DEFAULT_VERSION.redis}). clickhouse: ${ENGINE_VERSIONS.clickhouse.join(", ")} (default ${ENGINE_DEFAULT_VERSION.clickhouse}, the LTS and the only supported version). opensearch: ${ENGINE_VERSIONS.opensearch.join(", ")} (default ${ENGINE_DEFAULT_VERSION.opensearch}). Omitting this sends the default explicitly - the backend's own fallback is the oldest major it still supports.`),
|
|
592
|
+
username: z.string().regex(/^[a-z_][a-z0-9_-]{0,30}$/i).optional().describe("Database username (default: admin). Redis ignores this (auth is a single requirepass/password); it applies to postgres, mysql, clickhouse and opensearch. On clickhouse the customer user is `admin` - the built-in `default` user is platform-internal and not usable. On opensearch the customer user holds `all_access` and the internal `admin` superuser is never surfaced."),
|
|
451
593
|
custom_password: z.string().min(8).max(128).optional().describe("Database password (8-128 printable ASCII chars). Auto-generated if omitted."),
|
|
452
594
|
description: z.string().optional(),
|
|
453
595
|
cloud_credential_id: z.string().optional().describe("Credential PID from list_credentials. Use is_platform_managed=true credential for SelfHost's cloud, or is_platform_managed=false for BYOC. Always ask the user which they prefer."),
|
|
454
596
|
vpc_id: z.string().optional().describe("VPC ID - REQUIRED when using BYOC credentials (is_platform_managed=false). Use list_vpcs and only pick from the 'public' group. Not needed for platform-managed."),
|
|
455
|
-
multi_az: z.boolean().optional().describe("Enable multi-AZ replication (default: false). Required for any replica_count on clickhouse
|
|
456
|
-
replica_count: z.number().int().min(1).max(5).optional().describe("Replicas to provision alongside the primary when multi_az is on. postgres/mysql/redis: 1 or 2. clickhouse: 1-5, default 2 (1 adds a billed Keeper node -
|
|
597
|
+
multi_az: z.boolean().optional().describe("Enable multi-AZ replication (default: false). Required for any replica_count on clickhouse and opensearch. On opensearch it is PERMANENT - the only engine where HA cannot be toggled after create."),
|
|
598
|
+
replica_count: z.number().int().min(1).max(5).optional().describe("Replicas to provision alongside the primary when multi_az is on. postgres/mysql/redis: 1 or 2. clickhouse: 1-5, default 2 (1 adds a billed Keeper node). opensearch: buys DATA nodes and silently adds 3 billed manager nodes on top - 1 means 5 machines. See the engine notes."),
|
|
599
|
+
manager_instance_type: z.string().optional().describe("opensearch only: size of the 3 dedicated cluster-manager nodes when replica_count >= 1 (default t4g.medium, 4 GiB floor). They carry no search load, so they can be smaller than the data nodes."),
|
|
600
|
+
dashboards_enabled: z.boolean().optional().describe("opensearch only: install the OpenSearch Dashboards UI on port 5601. Needs 4 GiB RAM minimum (8 recommended) since it shares the endpoint node with the JVM. Can be changed later with set_opensearch_dashboards."),
|
|
601
|
+
confirm_node_count: z.boolean().optional().describe("opensearch only: required acknowledgement that replica_count >= 1 provisions 3 extra billed cluster-manager nodes. Omit it first to be told the real machine count, then set true once the user has seen the price."),
|
|
602
|
+
data_durability: z.enum(PG_DURABILITY_MODES).optional().describe("postgres only: how long a COMMIT waits for a standby. `async` = local ack (RPO > 0). `preferred` = wait when standbys are healthy, degrade when not (never blocks). `required` = RPO 0, and BLOCKS WRITES when it cannot be held. Defaults to `preferred` with multi_az, `async` without."),
|
|
603
|
+
sync_number: z.number().int().min(1).optional().describe("postgres only: how many standbys must ack a commit (default 1). Cannot exceed replica_count."),
|
|
604
|
+
no_auto_failover_ack: z.boolean().optional().describe("postgres only: required when data_durability=`required` with exactly 1 replica - that topology has NO automated failover, and this is the explicit acceptance of it."),
|
|
457
605
|
delete_protection: z.boolean().optional().describe("Prevent accidental deletion (default: true recommended)"),
|
|
458
606
|
backup_enabled: z.boolean().optional().describe("Enable automated backups (default: true recommended)"),
|
|
459
607
|
dlm_policy_config: dlmPolicySchema.optional().describe("Backup schedule config (required if backup_enabled)"),
|
|
460
608
|
allowed_cidr_ranges: z.array(cidrRangeSchema).max(24).optional().describe("IP whitelist (max 24 CIDR ranges)"),
|
|
461
609
|
tags: z.array(tagSchema).optional().describe("AWS resource tags"),
|
|
462
610
|
snapshot_id: z.string().optional().describe("Restore from a backup snapshot ID"),
|
|
463
|
-
|
|
464
|
-
vector_enabled: z.boolean().optional().describe("Install pgvector for embeddings / similarity search (postgres only). Enable-only - it cannot be turned off later, so confirm with the user first."),
|
|
611
|
+
extensions: z.array(z.string().min(1)).optional().describe("PostgreSQL extensions to install during provisioning, by catalog name, e.g. [\"pgvector\", \"pg_stat_statements\"]. postgres only. Read `list_postgres_extension_catalog` first - names are validated server-side and an incompatible one is a 422. None can be removed later."),
|
|
465
612
|
postgresql_config: z.record(z.unknown()).optional().describe("PostgreSQL parameter overrides (use preview_pg_config to see available params)"),
|
|
466
613
|
iam_role_arn: z.string().optional().describe("IAM role ARN for the EC2 instance"),
|
|
467
614
|
db_port: z.number().int().min(0).max(65535).optional().describe(`Database port (default ${ENGINE_DEFAULT_PORT.postgres} postgres, ${ENGINE_DEFAULT_PORT.mysql} mysql, ${ENGINE_DEFAULT_PORT.redis} redis, ${ENGINE_DEFAULT_PORT.clickhouse} clickhouse HTTP)`),
|
|
@@ -484,19 +631,16 @@ POSTGRES EXTENSIONS (neither is a separate engine - both are postgres + a flag):
|
|
|
484
631
|
if (params.db_version && !ENGINE_VERSIONS[engine].includes(params.db_version)) {
|
|
485
632
|
return { content: [{ type: "text", text: `'${params.db_version}' is not a supported ${engine} version. Supported: ${ENGINE_VERSIONS[engine].join(", ")}.` }] };
|
|
486
633
|
}
|
|
487
|
-
//
|
|
488
|
-
//
|
|
489
|
-
//
|
|
634
|
+
// `extensions` is postgres-only. The backend IGNORES the list on other engines, which
|
|
635
|
+
// would look like a successful "pgvector instance" that has none - so refuse instead of
|
|
636
|
+
// quietly dropping the request.
|
|
490
637
|
if (engine !== "postgres") {
|
|
491
|
-
const requested = [
|
|
492
|
-
params.timescaledb_enabled ? "timescaledb_enabled" : null,
|
|
493
|
-
params.vector_enabled ? "vector_enabled" : null,
|
|
494
|
-
].filter(Boolean);
|
|
638
|
+
const requested = params.extensions ?? [];
|
|
495
639
|
if (requested.length > 0) {
|
|
496
640
|
return {
|
|
497
641
|
content: [{
|
|
498
642
|
type: "text",
|
|
499
|
-
text:
|
|
643
|
+
text: `extensions (${requested.join(", ")}) ${requested.length > 1 ? "are" : "is"} PostgreSQL-only and would be IGNORED on ${engine} - the instance would come up with none. Create this as postgres, or drop the list.${engine === "clickhouse" ? " (For analytics workloads ClickHouse is already columnar - it needs no extension.)" : ""}${engine === "opensearch" ? " (OpenSearch ships k-NN vector search in the engine - it needs no extension for embeddings.)" : ""}`,
|
|
500
644
|
}],
|
|
501
645
|
};
|
|
502
646
|
}
|
|
@@ -520,6 +664,26 @@ POSTGRES EXTENSIONS (neither is a separate engine - both are postgres + a flag):
|
|
|
520
664
|
}],
|
|
521
665
|
};
|
|
522
666
|
}
|
|
667
|
+
else if (engine === "opensearch" && replicaCount !== undefined && replicaCount > 0) {
|
|
668
|
+
return {
|
|
669
|
+
content: [{
|
|
670
|
+
type: "text",
|
|
671
|
+
text: `OpenSearch replicas require multi_az=true - the backend refuses any replica_count without it. Pass multi_az=true (replica_count defaults to ${ENGINE_DEFAULT_REPLICAS.opensearch}), or drop replica_count for a single all-in-one node.\n\nBefore you do: multi_az=true with replica_count=${ENGINE_DEFAULT_REPLICAS.opensearch} provisions ${opensearchNodeCount(ENGINE_DEFAULT_REPLICAS.opensearch)} BILLED NODES (3 cluster managers + 2 data), and it cannot be undone later. Price it with estimate_instance_cost first.`,
|
|
672
|
+
}],
|
|
673
|
+
};
|
|
674
|
+
}
|
|
675
|
+
// The three cluster managers are as real as any other node on the bill, and nothing
|
|
676
|
+
// downstream says so: `replica_count` reads like "extra machines" everywhere else.
|
|
677
|
+
// Refuse to provision five machines for a caller who asked for two without being told.
|
|
678
|
+
if (needsOpensearchManagers(engine, replicaCount ?? 0) && params.confirm_node_count !== true) {
|
|
679
|
+
const total = opensearchNodeCount(replicaCount);
|
|
680
|
+
return {
|
|
681
|
+
content: [{
|
|
682
|
+
type: "text",
|
|
683
|
+
text: `replica_count=${replicaCount} on OpenSearch provisions ${total} NODES, not ${replicaCount + 1}: 3 dedicated cluster-manager nodes (${params.manager_instance_type ?? OPENSEARCH_DEFAULT_MANAGER_TYPE}) plus ${replicaCount + 1} data nodes (${params.instance_type}). Every one of them bills, and the managers inherit the data nodes' ${params.storage.size} GiB storage.\n\nThis topology is also PERMANENT - OpenSearch HA cannot be toggled off later.\n\nPrice it with estimate_instance_cost (type_of_dbms="opensearch", replica_count=${replicaCount}), show the user the total, then call again with confirm_node_count=true.`,
|
|
684
|
+
}],
|
|
685
|
+
};
|
|
686
|
+
}
|
|
523
687
|
// Hard RAM floor (backend MIN_RAM_GB). Best-effort pre-check so a doomed create fails
|
|
524
688
|
// here with a useful message instead of after the provisioning call.
|
|
525
689
|
const minRamGiB = ENGINE_MIN_RAM_GIB[engine];
|
|
@@ -534,6 +698,53 @@ POSTGRES EXTENSIONS (neither is a separate engine - both are postgres + a flag):
|
|
|
534
698
|
};
|
|
535
699
|
}
|
|
536
700
|
}
|
|
701
|
+
// PostgreSQL HA durability. Mirrors `validate_ha_durability_create_params!` so a doomed
|
|
702
|
+
// create explains itself instead of returning a bare 422, and so the write-blocking
|
|
703
|
+
// consequence of `required` is stated before any money is spent.
|
|
704
|
+
if (engine === "postgres") {
|
|
705
|
+
const mode = params.data_durability ?? (multiAz ? "preferred" : "async");
|
|
706
|
+
if (multiAz && mode !== "async") {
|
|
707
|
+
const syncNumber = params.sync_number ?? PG_DURABILITY_DEFAULT_SYNC_NUMBER;
|
|
708
|
+
const standbys = replicaCount ?? 0;
|
|
709
|
+
if (syncNumber > standbys) {
|
|
710
|
+
return {
|
|
711
|
+
content: [{
|
|
712
|
+
type: "text",
|
|
713
|
+
text: `sync_number ${syncNumber} needs ${syncNumber} HA standbys but this create asks for ${standbys}. Raise replica_count to at least ${syncNumber}, or lower sync_number.`,
|
|
714
|
+
}],
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
if (mode === "required" && standbys === 1 && params.no_auto_failover_ack !== true) {
|
|
718
|
+
return {
|
|
719
|
+
content: [{
|
|
720
|
+
type: "text",
|
|
721
|
+
text: "A 2-node `required` cluster (1 replica) has NO AUTOMATED FAILOVER: promoting the only standby is exactly what would lose the data `required` exists to protect, so the platform will not do it by itself. If the primary fails, someone has to intervene.\n\nThat is a deliberate trade - RPO 0 at the cost of automatic recovery. Either raise replica_count to 2 so failover is safe, or confirm the user accepts manual failover and pass no_auto_failover_ack=true.",
|
|
722
|
+
}],
|
|
723
|
+
};
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
else if (params.data_durability || params.sync_number !== undefined || params.no_auto_failover_ack !== undefined) {
|
|
728
|
+
return {
|
|
729
|
+
content: [{
|
|
730
|
+
type: "text",
|
|
731
|
+
text: `data_durability / sync_number / no_auto_failover_ack are PostgreSQL-only and would be ignored on ${engine}. Drop them, or create this as postgres.`,
|
|
732
|
+
}],
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
// Managers have their own floor, checked separately because they are usually smaller
|
|
736
|
+
// than the data nodes - a 2 GiB manager OOMs its JVM under quorum load.
|
|
737
|
+
if (engine === "opensearch" && params.manager_instance_type && needsOpensearchManagers(engine, replicaCount ?? 0)) {
|
|
738
|
+
const managerRam = await lookupInstanceMemoryGiB(params.region, params.manager_instance_type);
|
|
739
|
+
if (managerRam !== null && managerRam < OPENSEARCH_MANAGER_RAM_FLOOR_GIB) {
|
|
740
|
+
return {
|
|
741
|
+
content: [{
|
|
742
|
+
type: "text",
|
|
743
|
+
text: `manager_instance_type ${params.manager_instance_type} has ${managerRam} GiB RAM, below the ${OPENSEARCH_MANAGER_RAM_FLOOR_GIB} GiB floor for an OpenSearch cluster manager (the backend rejects it). ${OPENSEARCH_DEFAULT_MANAGER_TYPE} is the smallest production-eligible manager.`,
|
|
744
|
+
}],
|
|
745
|
+
};
|
|
746
|
+
}
|
|
747
|
+
}
|
|
537
748
|
// Build request body - only include defined fields
|
|
538
749
|
const body = {
|
|
539
750
|
organization_id: orgId,
|
|
@@ -568,6 +779,16 @@ POSTGRES EXTENSIONS (neither is a separate engine - both are postgres + a flag):
|
|
|
568
779
|
// Resolved, not raw: multi-AZ ClickHouse without an explicit count would 422 otherwise.
|
|
569
780
|
if (replicaCount !== undefined)
|
|
570
781
|
body.replica_count = replicaCount;
|
|
782
|
+
if (params.manager_instance_type)
|
|
783
|
+
body.manager_instance_type = params.manager_instance_type;
|
|
784
|
+
if (params.dashboards_enabled !== undefined)
|
|
785
|
+
body.dashboards_enabled = params.dashboards_enabled;
|
|
786
|
+
if (params.data_durability)
|
|
787
|
+
body.data_durability = params.data_durability;
|
|
788
|
+
if (params.sync_number !== undefined)
|
|
789
|
+
body.sync_number = params.sync_number;
|
|
790
|
+
if (params.no_auto_failover_ack !== undefined)
|
|
791
|
+
body.no_auto_failover_ack = params.no_auto_failover_ack;
|
|
571
792
|
if (params.delete_protection !== undefined)
|
|
572
793
|
body.delete_protection = params.delete_protection;
|
|
573
794
|
if (params.backup_enabled !== undefined)
|
|
@@ -580,10 +801,8 @@ POSTGRES EXTENSIONS (neither is a separate engine - both are postgres + a flag):
|
|
|
580
801
|
body.tags = params.tags;
|
|
581
802
|
if (params.snapshot_id)
|
|
582
803
|
body.snapshot_id = params.snapshot_id;
|
|
583
|
-
if (params.
|
|
584
|
-
body.
|
|
585
|
-
if (params.vector_enabled)
|
|
586
|
-
body.vector_enabled = true;
|
|
804
|
+
if (params.extensions?.length)
|
|
805
|
+
body.extensions = params.extensions;
|
|
587
806
|
if (params.postgresql_config)
|
|
588
807
|
body.postgresql_config = params.postgresql_config;
|
|
589
808
|
if (params.iam_role_arn)
|
|
@@ -615,19 +834,16 @@ POSTGRES EXTENSIONS (neither is a separate engine - both are postgres + a flag):
|
|
|
615
834
|
`Group ID: ${data.group_id}`,
|
|
616
835
|
`Organization: ${data.organization_id}`,
|
|
617
836
|
];
|
|
618
|
-
if (engine === "postgres" &&
|
|
619
|
-
const
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
lines.push("", `${extensions.join(" + ")} ${extensions.length > 1 ? "are" : "is"} installed during provisioning (no restart needed at create).`);
|
|
624
|
-
if (params.timescaledb_enabled) {
|
|
625
|
-
lines.push("- Start with: CREATE TABLE ... then SELECT create_hypertable('your_table', 'time');", "- The timescaledb_* metrics (hypertable/chunk counts, compression ratio, total size) join this instance's `postgresql.database` alert set.");
|
|
837
|
+
if (engine === "postgres" && params.extensions?.length) {
|
|
838
|
+
const requested = params.extensions;
|
|
839
|
+
lines.push("", `Extensions installed during provisioning (no restart needed on this path): ${requested.join(", ")}.`, "- Each starts as a `pending` row and is only marked `enabled` once it is created AND its smoke test passes. The instance is not marked ready while any is missing, so check `list_instance_extensions` if readiness stalls.");
|
|
840
|
+
if (requested.includes("timescaledb")) {
|
|
841
|
+
lines.push("- TimescaleDB: start with CREATE TABLE ... then SELECT create_hypertable('your_table', 'time');", "- The timescaledb_* metrics (hypertable/chunk counts, compression ratio, total size) join this instance's `postgresql.database` alert set.");
|
|
626
842
|
}
|
|
627
|
-
if (
|
|
628
|
-
lines.push("-
|
|
843
|
+
if (requested.includes("pgvector") || requested.includes("vector")) {
|
|
844
|
+
lines.push("- pgvector: the `vector` column type and similarity operators are ready to use.");
|
|
629
845
|
}
|
|
630
|
-
lines.push(
|
|
846
|
+
lines.push("- None of them can be removed later - there is no disable path.");
|
|
631
847
|
}
|
|
632
848
|
if (engine === "clickhouse") {
|
|
633
849
|
lines.push("", `ClickHouse ${dbVersion}`, `- Connect over HTTP on port ${ENGINE_DEFAULT_PORT.clickhouse} (native TCP on ${CLICKHOUSE_NATIVE_PORT}).`);
|
|
@@ -638,7 +854,21 @@ POSTGRES EXTENSIONS (neither is a separate engine - both are postgres + a flag):
|
|
|
638
854
|
}
|
|
639
855
|
lines.push("- Provisioning a ClickHouse cluster runs ~11 tasks and takes longer than Postgres/MySQL. Poll refresh_instance_group.");
|
|
640
856
|
}
|
|
641
|
-
lines.push("- Follow-ups: enable_chproxy for connection pooling, database-users for SQL users
|
|
857
|
+
lines.push("- Follow-ups: enable_chproxy for connection pooling, database-users for SQL users. No PITR, no config tuning and no version change on this engine.");
|
|
858
|
+
}
|
|
859
|
+
if (engine === "opensearch") {
|
|
860
|
+
lines.push("", `OpenSearch ${dbVersion}`, `- Connect over HTTPS on port ${ENGINE_DEFAULT_PORT.opensearch} - never plaintext. There is no database name to supply.`, "- Quick check once ready: curl -k -u \"<username>:<password>\" \"https://<dns_name>:" + ENGINE_DEFAULT_PORT.opensearch + "/_cluster/health?pretty\"", "- Use `dns_name`, not the public DNS: it is the endpoint node's CNAME and it survives failover and resize.", "- TLS: the cert's SAN covers the node's private IP, not the public endpoint, so strict hostname verification fails even with the cluster CA trusted. Use CA-trust with relaxed hostname checking, or connect from inside the VPC. `-k` is for quick starts only, and the CA is not downloadable through the API yet.");
|
|
861
|
+
if (multiAz) {
|
|
862
|
+
const dataNodes = replicaCount + 1;
|
|
863
|
+
lines.push(`- Cluster: ${dataNodes} data/ingest nodes + ${OPENSEARCH_MANAGER_COUNT} dedicated cluster managers = ${opensearchNodeCount(replicaCount)} NODES, all billed. The managers hold the leader-election quorum and carry no indexing or search load.`, "- This topology is PERMANENT. multi_az cannot be toggled on an OpenSearch cluster in either direction - changing it means creating a new cluster and restoring into it.", "- Poll `ha_ready` and `readiness_detail` rather than inferring health from `status`; `opensearch_node_role` tells you which node is which.");
|
|
864
|
+
}
|
|
865
|
+
else {
|
|
866
|
+
lines.push("- Single all-in-one node: no redundancy, so this is a dev / non-HA shape. HA cannot be added later - it needs a new cluster at replica_count >= 1 and a restore into it.");
|
|
867
|
+
}
|
|
868
|
+
if (params.dashboards_enabled) {
|
|
869
|
+
lines.push(`- Dashboards was requested and installs on port ${OPENSEARCH_DASHBOARDS_PORT} AFTER the instance is ready - it is never part of provisioning or readiness. Watch it with get_opensearch_dashboards; a \`failed\` or \`degraded\` state leaves the cluster itself perfectly healthy.`);
|
|
870
|
+
}
|
|
871
|
+
lines.push("- Follow-ups: database-users for security-plugin users, set_opensearch_dashboards for the UI. No pooling, no config tuning, no PITR and no version change on this engine.", "- Vector search (k-NN) is already in the engine - no extension needed.");
|
|
642
872
|
}
|
|
643
873
|
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
644
874
|
});
|
|
@@ -673,6 +903,12 @@ READINESS IS ONE FIELD. \`ready\` is computed server-side and is the field to tr
|
|
|
673
903
|
|
|
674
904
|
If an instance has been provisioning for more than about 15 minutes it is considered STUCK rather than slow - say so instead of advising the user to keep waiting.
|
|
675
905
|
|
|
906
|
+
NOT EVERY NODE IN A GROUP IS A DATABASE. Two fields say which is which, and reading them avoids pointing a user at a node that cannot do what they asked:
|
|
907
|
+
- \`clickhouse_node_role\` - \`keeper_only\` marks the Raft arbiter. It runs clickhouse-keeper, not clickhouse-server, so it holds no table data: no query stats, no data backup, no scaling policy. It is not a node to connect to, but it IS a node on the bill.
|
|
908
|
+
- \`opensearch_node_role\` - which role this node carries in the cluster. Only the node with \`opensearch_endpoint: true\` is the client-facing 9200/DNS target; manager-only nodes serve no traffic and carry no data.
|
|
909
|
+
|
|
910
|
+
A HOT STANDBY IS A READABLE REPLICA, NOT A WRITE TARGET, and it also cannot be scaled or reconfigured directly - those operations belong to its primary and the backend refuses them on a replica, naming the primary. When a user asks to change something on a node that turns out to be a replica, act on the primary instead of relaying a confusing refusal.
|
|
911
|
+
|
|
676
912
|
\`deprecated_at\`, when set, marks the machine type as scheduled for retirement. Worth surfacing unprompted.`, {
|
|
677
913
|
pid: z.string().optional().describe("Instance PID (e.g. awsinst_xxx)"),
|
|
678
914
|
instance_id: z.string().optional().describe("AWS EC2 instance ID (alternative to pid)"),
|
|
@@ -708,7 +944,7 @@ Metric sets (same names alert rules use):
|
|
|
708
944
|
- system: cpu_percent, memory_percent, disk_usage_percent, load_avg_1/5/15, swap_usage_percent, cpu_io_wait_percent
|
|
709
945
|
- network: recv_bytes_per_sec, sent_bytes_per_sec
|
|
710
946
|
- storage: disk_used_percent, disk_free_bytes, inodes_used_percent, read/write_io_per_second, ... reported PER MOUNT POINT and keyed \`storage[/mount]\` in the summary. Read the engine's data volume (e.g. \`storage[/selfhostdev/postgresql]\`), not \`storage[/]\` or \`storage[/boot]\` - a full /boot says nothing about database capacity.
|
|
711
|
-
- <engine>.database: postgresql.database | mysql.database | redis.database | clickhouse.database - the engine's own counters. \`create_alert_rule\` carries the complete per-engine allow-list; these are the same names.
|
|
947
|
+
- <engine>.database: postgresql.database | mysql.database | redis.database | clickhouse.database | opensearch.database - the engine's own counters. \`create_alert_rule\` carries the complete per-engine allow-list; these are the same names.
|
|
712
948
|
- pgbouncer.stats appears once pooling is enabled. It is READABLE here but NOT alertable - the alert-rule catalog has no such metric_type, so a rule on it is rejected.
|
|
713
949
|
|
|
714
950
|
On a replicated instance \`active_replica_count\` is the one to watch for a quietly-degraded cluster: it drops without the instance's own status changing. Ratio metrics (\`cache_hit_ratio\`, \`innodb_buffer_pool_hit_ratio\`, \`mark_cache_hit_ratio\`) are already on a 0-100 scale - do not multiply them by 100.
|
|
@@ -720,7 +956,7 @@ An empty result usually means the agent has not reported in that window (a stopp
|
|
|
720
956
|
.describe("Inclusive start date, YYYY-MM-DD (default: yesterday). The window covers whole days."),
|
|
721
957
|
end_date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "YYYY-MM-DD").optional()
|
|
722
958
|
.describe("Inclusive end date, YYYY-MM-DD (default: today)"),
|
|
723
|
-
metric_type: z.enum(["system", "network", "storage", "postgresql.database", "mysql.database", "redis.database", "clickhouse.database"]).optional()
|
|
959
|
+
metric_type: z.enum(["system", "network", "storage", "postgresql.database", "mysql.database", "redis.database", "clickhouse.database", "opensearch.database"]).optional()
|
|
724
960
|
.describe("Only return this metric set. Cheaper than filtering client-side."),
|
|
725
961
|
raw: z.boolean().optional().describe("Return every heartbeat frame instead of the summary. Verbose - use a narrow window and a single instance."),
|
|
726
962
|
}, async ({ organization_id, instance_pid, start_date, end_date, metric_type, raw }) => {
|
|
@@ -776,12 +1012,12 @@ An empty result usually means the agent has not reported in that window (a stopp
|
|
|
776
1012
|
// history - activity counts, a `failover_completed` state, achievement badges - with no
|
|
777
1013
|
// button to trigger one. The endpoint is an operator escape hatch, so it stays out of an
|
|
778
1014
|
// LLM's reach: it drops every connection and the old master never returns as master.
|
|
779
|
-
server.tool("update_instance", "Update an instance's configuration. Some changes are synchronous (db_port, backup, CIDR ranges), others are async (instance_type, multi_az, storage). Cannot update replicas - update the master instead.
|
|
1015
|
+
server.tool("update_instance", "Update an instance's configuration. Some changes are synchronous (db_port, backup, CIDR ranges), others are async (instance_type, multi_az, storage). Cannot update replicas - update the master instead. Cannot change db_version on any engine: the platform withdrew the in-place ClickHouse upgrade in selfhost#2580, so a version change is now a new instance plus a restore or migration into it. Cannot toggle multi_az on OpenSearch - its HA is a cluster topology chosen at create. Not handled here either: enabling postgres extensions (see the extension tools) or the OpenSearch Dashboards UI (set_opensearch_dashboards).", {
|
|
780
1016
|
instance_id: z.string().min(1, "Instance PID is required"),
|
|
781
1017
|
identifier: z.string().optional().describe("New instance name"),
|
|
782
1018
|
delete_protection: z.boolean().optional(),
|
|
783
1019
|
instance_type: z.string().optional().describe("New EC2 instance type (triggers async resize)"),
|
|
784
|
-
multi_az: z.boolean().optional().describe("Enable/disable multi-AZ (triggers async operation)"),
|
|
1020
|
+
multi_az: z.boolean().optional().describe("Enable/disable multi-AZ (triggers async operation). NOT AVAILABLE ON OPENSEARCH in either direction - its HA is a cluster topology fixed at create."),
|
|
785
1021
|
backup_enabled: z.boolean().optional(),
|
|
786
1022
|
db_port: z.number().int().min(0).max(65535).optional(),
|
|
787
1023
|
security_group_ids: z.array(z.string()).optional(),
|
|
@@ -819,40 +1055,139 @@ An empty result usually means the agent has not reported in that window (a stopp
|
|
|
819
1055
|
],
|
|
820
1056
|
};
|
|
821
1057
|
});
|
|
822
|
-
server.tool("
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
-
|
|
826
|
-
|
|
827
|
-
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
1058
|
+
server.tool("set_postgres_durability", `Change a PostgreSQL group's HA durability mode - how long a COMMIT waits for a standby to flush the WAL before the primary acks it. This is the RPO dial.
|
|
1059
|
+
|
|
1060
|
+
THE THREE MODES, and the only difference that matters is what happens when no standby is healthy:
|
|
1061
|
+
- \`async\` - the primary acks locally. Fastest, and RPO > 0: commits can be lost if the primary dies before a standby has them.
|
|
1062
|
+
- \`preferred\` - waits while standbys are healthy, and DEGRADES to no-wait when none are. It never blocks writes, which is exactly why it cannot be described as zero-data-loss.
|
|
1063
|
+
- \`required\` - RPO 0, honestly implemented: dead standby names stay in the wait set, so when the SLA cannot be held **THE PRIMARY STOPS ACCEPTING WRITES** rather than acking data no survivor holds. Availability is traded for durability, deliberately.
|
|
1064
|
+
|
|
1065
|
+
ONLY \`required\` WHILE \`sla_held\` IS TRUE MAY BE CALLED ZERO DATA LOSS. Read \`sla_held\` and \`degraded\` from the response before telling a user their data is safe: \`required\` with \`sla_held: false\` means writes are being refused right now, and \`preferred\` with \`degraded: true\` means it is silently acking locally.
|
|
1066
|
+
|
|
1067
|
+
RULES THE BACKEND ENFORCES:
|
|
1068
|
+
- PostgreSQL only, and on the MASTER only - replicas follow the group mode (a 422 names the primary).
|
|
1069
|
+
- THIS MUST BE SENT ALONE. Durability changes cannot ride along with any other instance update, so do not try to combine it with \`update_instance\`.
|
|
1070
|
+
- Moving TO \`preferred\` or \`required\` needs standbys actually streaming; without them the move is refused ("no streaming standbys to wait on"). Moving to \`async\` is always allowed - it is the honest escape hatch from a stalled \`required\` primary.
|
|
1071
|
+
- \`sync_number\` cannot exceed the group's HA standby count.
|
|
1072
|
+
- \`required\` with exactly one standby needs \`no_auto_failover_ack\` - see below.
|
|
1073
|
+
|
|
1074
|
+
IF A PRIMARY IS STALLED because \`required\` cannot be held, the unwedge is moving to \`preferred\` (which starts shrinking the wait set) or \`async\` (which removes the wait). That is the answer to "my writes are hanging" on a \`required\` cluster, and it is worth offering immediately rather than diagnosing further.`, {
|
|
1075
|
+
instance_id: z.string().min(1).describe("The MASTER instance's PID - not a replica"),
|
|
1076
|
+
data_durability: z.enum(PG_DURABILITY_MODES).optional().describe("Target mode. Omit to keep the current one and change only sync_number."),
|
|
1077
|
+
sync_number: z.number().int().min(1).optional().describe(`How many standbys must ack (default ${PG_DURABILITY_DEFAULT_SYNC_NUMBER}). Cannot exceed the group's HA standby count.`),
|
|
1078
|
+
no_auto_failover_ack: z.boolean().optional().describe("Required for `required` with exactly 1 standby: that topology has no automated failover."),
|
|
1079
|
+
confirm: z.boolean().describe("Must be true. Moving to `required` can block writes; moving to `async` gives up zero-data-loss."),
|
|
1080
|
+
}, async ({ instance_id, data_durability, sync_number, no_auto_failover_ack, confirm }) => {
|
|
1081
|
+
if (data_durability === undefined && sync_number === undefined && no_auto_failover_ack === undefined) {
|
|
1082
|
+
return { content: [{ type: "text", text: "Nothing to change - pass at least one of data_durability / sync_number / no_auto_failover_ack." }] };
|
|
1083
|
+
}
|
|
836
1084
|
if (!confirm) {
|
|
1085
|
+
const consequence = data_durability === "required"
|
|
1086
|
+
? "`required` means the primary STOPS ACCEPTING WRITES whenever the synchronous standby set cannot be held. That is the point of it - RPO 0 - but it converts a standby failure into a write outage. With exactly one standby it also disables automated failover."
|
|
1087
|
+
: data_durability === "async"
|
|
1088
|
+
? "`async` removes the commit wait entirely: writes get faster and RPO becomes greater than zero, so commits can be lost if the primary dies. Any zero-data-loss claim stops being true."
|
|
1089
|
+
: data_durability === "preferred"
|
|
1090
|
+
? "`preferred` waits while standbys are healthy and silently degrades to local acks when they are not, so it never blocks writes and never guarantees zero data loss."
|
|
1091
|
+
: "Changing sync_number changes how many standbys must ack each commit; raising it past the number of healthy standbys will stall writes in `required` mode.";
|
|
837
1092
|
return {
|
|
838
1093
|
content: [{
|
|
839
1094
|
type: "text",
|
|
840
|
-
text: `About to
|
|
1095
|
+
text: `About to change durability on ${instance_id}${data_durability ? ` to \`${data_durability}\`` : ""}${sync_number !== undefined ? ` with sync_number ${sync_number}` : ""}.\n\n${consequence}\n\nCall again with confirm=true.`,
|
|
1096
|
+
}],
|
|
1097
|
+
};
|
|
1098
|
+
}
|
|
1099
|
+
// The backend refuses a durability change that carries any other key, so this tool
|
|
1100
|
+
// sends nothing else - same contract as set_postgres_tls.
|
|
1101
|
+
const body = {};
|
|
1102
|
+
if (data_durability)
|
|
1103
|
+
body.data_durability = data_durability;
|
|
1104
|
+
if (sync_number !== undefined)
|
|
1105
|
+
body.sync_number = sync_number;
|
|
1106
|
+
if (no_auto_failover_ack !== undefined)
|
|
1107
|
+
body.no_auto_failover_ack = no_auto_failover_ack;
|
|
1108
|
+
const result = await apiRequest(`/aws/v1/instances/${instance_id}`, { method: "PATCH", body, toolName: "set_postgres_durability", skipOrgInjection: true });
|
|
1109
|
+
if (!result.success) {
|
|
1110
|
+
return { content: [{ type: "text", text: `Failed to change durability (${result.statusCode}): ${result.message}` }] };
|
|
1111
|
+
}
|
|
1112
|
+
const d = result.data ?? {};
|
|
1113
|
+
const notes = [`Durability is now \`${d.data_durability ?? data_durability}\`.`];
|
|
1114
|
+
if (d.data_durability === "required") {
|
|
1115
|
+
notes.push(d.sla_held
|
|
1116
|
+
? "`sla_held` is true, so this cluster is genuinely holding RPO 0 right now - it is the one state in which zero data loss is an accurate claim."
|
|
1117
|
+
: "⚠️ `sla_held` is FALSE: the synchronous set is not being held, so the primary is refusing writes. Move to `preferred` or `async` to unwedge it, or bring a standby back.");
|
|
1118
|
+
if (d.auto_failover_allowed === false) {
|
|
1119
|
+
notes.push("Automated failover is OFF for this group - a primary failure needs manual intervention.");
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
else if (d.data_durability === "preferred" && d.degraded) {
|
|
1123
|
+
notes.push("⚠️ `degraded` is true: no standby is healthy, so commits are being acked locally. Writes are flowing, but this is NOT zero data loss - do not describe it as such.");
|
|
1124
|
+
}
|
|
1125
|
+
else if (d.data_durability === "async") {
|
|
1126
|
+
notes.push("Commits now ack locally. RPO is greater than zero: any zero-data-loss claim for this cluster is no longer true.");
|
|
1127
|
+
}
|
|
1128
|
+
return {
|
|
1129
|
+
content: [
|
|
1130
|
+
{ type: "text", text: notes.join("\n\n") },
|
|
1131
|
+
{ type: "text", text: JSON.stringify(result.data, null, 2) },
|
|
1132
|
+
],
|
|
1133
|
+
};
|
|
1134
|
+
});
|
|
1135
|
+
server.tool("get_opensearch_dashboards", `Read the OpenSearch Dashboards state for an instance - the opt-in Kibana-style UI on port ${OPENSEARCH_DASHBOARDS_PORT}.
|
|
1136
|
+
|
|
1137
|
+
Returns \`dashboards_enabled\`, \`dashboards_status\` (\`disabled\` | \`installing\` | \`running\` | \`degraded\` | \`failed\`), \`dashboards_url\`, the instance's \`memory_gb\`, and two RAM verdicts: \`dashboards_supported\` (>= ${DASHBOARDS_MIN_RAM_GIB} GiB, the hard floor) and \`dashboards_recommended\` (>= ${DASHBOARDS_RECOMMENDED_RAM_GIB} GiB). Check \`dashboards_supported\` BEFORE offering to enable it - below the floor the enable is rejected outright.
|
|
1138
|
+
|
|
1139
|
+
DASHBOARDS FAILING DOES NOT MEAN THE CLUSTER IS UNHEALTHY. It is co-located on the endpoint node as a Node.js process next to the JVM, and it is deliberately fail-soft and outside provisioning and readiness: the instance is ready when port ${ENGINE_DEFAULT_PORT.opensearch} is, whatever Dashboards is doing. Report a \`failed\` or \`degraded\` state as a separate, non-blocking fact, never as a broken database.
|
|
1140
|
+
|
|
1141
|
+
422 on any non-OpenSearch instance.`, {
|
|
1142
|
+
instance_id: z.string().min(1).describe("Instance PID (e.g. awsinst_xxx). Must be the cluster's endpoint node."),
|
|
1143
|
+
}, async ({ instance_id }) => {
|
|
1144
|
+
const result = await apiRequest(`/aws/v1/instances/${instance_id}/dashboards`, {
|
|
1145
|
+
toolName: "get_opensearch_dashboards",
|
|
1146
|
+
skipOrgInjection: true,
|
|
1147
|
+
});
|
|
1148
|
+
if (!result.success) {
|
|
1149
|
+
return { content: [{ type: "text", text: `Failed to get Dashboards state (${result.statusCode}): ${result.message}` }] };
|
|
1150
|
+
}
|
|
1151
|
+
return { content: [{ type: "text", text: JSON.stringify(result.data, null, 2) }] };
|
|
1152
|
+
});
|
|
1153
|
+
server.tool("set_opensearch_dashboards", `Turn the OpenSearch Dashboards UI on or off on an existing cluster (port ${OPENSEARCH_DASHBOARDS_PORT}). Unlike most of this engine's shape, this one IS a day-2 toggle in both directions.
|
|
1154
|
+
|
|
1155
|
+
Call \`get_opensearch_dashboards\` first and check \`dashboards_supported\`: the endpoint node needs ${DASHBOARDS_MIN_RAM_GIB} GiB RAM to enable at all (${DASHBOARDS_RECOMMENDED_RAM_GIB} GiB recommended, since Dashboards is a Node.js process sharing the box with the OpenSearch JVM). Below the floor this is a 422.
|
|
1156
|
+
|
|
1157
|
+
Other requirements the backend enforces: OpenSearch only, the cluster's ENDPOINT node only (not a manager or a non-endpoint data node), status \`running\` or \`provisioned\`, and a registered agent to enable.
|
|
1158
|
+
|
|
1159
|
+
Enabling also opens ${OPENSEARCH_DASHBOARDS_PORT} on the security group. Poll \`get_opensearch_dashboards\` for \`installing\` → \`running\`; a \`failed\` outcome leaves the cluster itself healthy and serving on ${ENGINE_DEFAULT_PORT.opensearch}.`, {
|
|
1160
|
+
instance_id: z.string().min(1).describe("Instance PID of the cluster's endpoint node"),
|
|
1161
|
+
enabled: z.boolean().describe("true installs and starts Dashboards; false stops it and closes the port."),
|
|
1162
|
+
confirm: z.boolean().describe("Must be true. Enabling adds a process competing with the JVM for memory on the endpoint node and opens another port; disabling takes the UI away from anyone using it."),
|
|
1163
|
+
}, async ({ instance_id, enabled, confirm }) => {
|
|
1164
|
+
if (!confirm) {
|
|
1165
|
+
return {
|
|
1166
|
+
content: [{
|
|
1167
|
+
type: "text",
|
|
1168
|
+
text: enabled
|
|
1169
|
+
? `About to enable OpenSearch Dashboards on ${instance_id}. It runs on the endpoint node alongside the OpenSearch JVM (so it competes for memory) and opens port ${OPENSEARCH_DASHBOARDS_PORT}. Check \`dashboards_supported\` via get_opensearch_dashboards first.\n\nCall again with confirm=true.`
|
|
1170
|
+
: `About to disable OpenSearch Dashboards on ${instance_id}. The UI stops and port ${OPENSEARCH_DASHBOARDS_PORT} closes; the cluster keeps serving on ${ENGINE_DEFAULT_PORT.opensearch}.\n\nCall again with confirm=true.`,
|
|
841
1171
|
}],
|
|
842
1172
|
};
|
|
843
1173
|
}
|
|
844
1174
|
const result = await apiRequest(`/aws/v1/instances/${instance_id}`, {
|
|
845
1175
|
method: "PATCH",
|
|
846
|
-
body: {
|
|
847
|
-
toolName: "
|
|
1176
|
+
body: { dashboards_enabled: enabled },
|
|
1177
|
+
toolName: "set_opensearch_dashboards",
|
|
848
1178
|
skipOrgInjection: true,
|
|
849
1179
|
});
|
|
850
1180
|
if (!result.success) {
|
|
851
|
-
return { content: [{ type: "text", text: `Failed to
|
|
1181
|
+
return { content: [{ type: "text", text: `Failed to ${enabled ? "enable" : "disable"} Dashboards (${result.statusCode}): ${result.message}` }] };
|
|
852
1182
|
}
|
|
853
1183
|
return {
|
|
854
1184
|
content: [
|
|
855
|
-
{
|
|
1185
|
+
{
|
|
1186
|
+
type: "text",
|
|
1187
|
+
text: enabled
|
|
1188
|
+
? `Dashboards enablement requested. Poll get_opensearch_dashboards until dashboards_status reaches \`running\`, then open dashboards_url. A \`failed\` state is a Dashboards problem only - the cluster stays healthy on ${ENGINE_DEFAULT_PORT.opensearch}.`
|
|
1189
|
+
: "Dashboards disabled. The port is closed and the cluster is unaffected.",
|
|
1190
|
+
},
|
|
856
1191
|
{ type: "text", text: JSON.stringify(result.data, null, 2) },
|
|
857
1192
|
],
|
|
858
1193
|
};
|