postgresai 0.17.0-dev.8 → 0.17.0-dev.9
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 +32 -11
- package/dist/bin/postgres-ai.js +337 -84
- package/dist/sql/02.extensions.sql +1 -1
- package/dist/sql/04.optional_rds.sql +2 -1
- package/dist/sql/sql/02.extensions.sql +1 -1
- package/dist/sql/sql/04.optional_rds.sql +2 -1
- package/package.json +1 -1
- package/sql/02.extensions.sql +1 -1
- package/sql/04.optional_rds.sql +2 -1
package/README.md
CHANGED
|
@@ -140,10 +140,20 @@ Before any grant runs, one `-- scope:` line lists exactly what the run grants th
|
|
|
140
140
|
monitoring role, derived from the steps about to run (so it shrinks under `--skip-optional-permissions`
|
|
141
141
|
and is not printed on `--reset-password`, which grants nothing). With `--json` it goes to stderr.
|
|
142
142
|
|
|
143
|
-
`channel_binding=require` in a URI or conninfo string enables SCRAM-SHA-256-PLUS when the server
|
|
143
|
+
For `prepare-db`, `channel_binding=require` in a URI or conninfo string enables SCRAM-SHA-256-PLUS when the server
|
|
144
144
|
offers it, disables the plaintext retry that `sslmode=prefer` would otherwise do, and is rejected
|
|
145
145
|
together with `sslmode=disable`. The mechanism actually negotiated is not enforced by the driver.
|
|
146
146
|
|
|
147
|
+
The collector cannot do channel binding. `mon targets add`, `mon local-install --db-url`, and
|
|
148
|
+
`pgai connect` remove `channel_binding` from the saved/box URL. With `require`, they refuse
|
|
149
|
+
`sslmode=disable` or `allow`; keep `verify-full` with a warning; and upgrade `require`, `prefer`,
|
|
150
|
+
`verify-ca` or unset to `verify-full` after testing certificate and hostname verification with
|
|
151
|
+
Node's default trust store (or the CA file in `sslrootcert`). Authentication or other errors after
|
|
152
|
+
a verified TLS handshake do not block the upgrade. If verification fails, provide
|
|
153
|
+
`sslrootcert=<CA file>` with `sslmode=verify-full`, or explicitly accept the downgrade by removing
|
|
154
|
+
`channel_binding=require`. Successful upgrades print the collector warning and
|
|
155
|
+
`upgraded to sslmode=verify-full`. `channel_binding=prefer` or `disable` is removed with a note.
|
|
156
|
+
|
|
147
157
|
### ClickHouse Cloud host metrics
|
|
148
158
|
|
|
149
159
|
For a ClickHouse Managed Postgres target, `mon local-install --db-url` and `mon targets add` also set up
|
|
@@ -215,9 +225,13 @@ terminal, or with `--json`, it only points to `pgai connect`.
|
|
|
215
225
|
| `--self-hosted` | run the stack on this machine (`mon local-install`) instead of PostgresAI Cloud |
|
|
216
226
|
| `--reset-password` | `postgres_ai_mon` exists and its password is lost: set a new one (see below) |
|
|
217
227
|
| `--wait <minutes>` | how long to wait for the monitoring box; `0` does not wait (default 20) |
|
|
218
|
-
| `-y, --yes` | never prompt (`connect` does not start the browser sign-in; `disconnect` does not ask) |
|
|
228
|
+
| `-y, --yes` | never prompt; accept a billed box's price (`connect` does not start the browser sign-in; `disconnect` does not ask) |
|
|
229
|
+
| `--coupon <code>` | promotion code for the organization's monitoring subscription |
|
|
219
230
|
| `--json` | JSON output, also on a terminal |
|
|
220
231
|
|
|
232
|
+
Without `--yes`, a non-interactive run for a billed box returns `action_required` with the price
|
|
233
|
+
and asks you to re-run with `--yes` to accept it.
|
|
234
|
+
|
|
221
235
|
| Environment | |
|
|
222
236
|
|---|---|
|
|
223
237
|
| `PGAI_API_KEY` | the API key, instead of signing in (agents, CI) |
|
|
@@ -250,13 +264,19 @@ used: set `PGAI_MON_PASSWORD`, or turn TLS on; if nobody has the password, disco
|
|
|
250
264
|
other databases, then `--reset-password` with `PGAI_MON_PASSWORD` set to a new one, and connect
|
|
251
265
|
them again with it (`connect` names them).
|
|
252
266
|
|
|
253
|
-
When
|
|
254
|
-
|
|
255
|
-
passwords for this host
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
267
|
+
When preparing an existing role with a supplied password (an admin URL with `PGAI_MON_PASSWORD`,
|
|
268
|
+
or the role's own URL), `connect` also tries a random password as `postgres_ai_mon` to learn
|
|
269
|
+
whether the server checks passwords for this host. Successful PostgresAI Cloud preparation makes
|
|
270
|
+
two attempts: one in the check before the price, and one in preparation. Stopping after the check
|
|
271
|
+
(for example, without accepting the price) makes one. Successful `--self-hosted` preparation
|
|
272
|
+
makes one. Creating or resetting the role with an admin URL, using the platform's stored password,
|
|
273
|
+
or returning an existing box's status makes no random-password attempts. A rejected supplied
|
|
274
|
+
password stops before the random-password probe; that rejection is itself a failed login.
|
|
275
|
+
On a server that checks passwords, each random-password attempt adds one
|
|
276
|
+
`password authentication failed for user "postgres_ai_mon"` line to the server log and counts
|
|
277
|
+
toward failed-login policies (credcheck, fail2ban). On a server that does not (`trust`), `next`
|
|
278
|
+
says that the password was not checked; a password from `PGPASSWORD` is then not used: put it
|
|
279
|
+
in the URL.
|
|
260
280
|
|
|
261
281
|
A role that is not a superuser creates `postgres_ai_mon` only if it can run the whole
|
|
262
282
|
preparation: `CREATEROLE`, `CREATE` on the database, `pg_stat_statements` already installed, and
|
|
@@ -264,7 +284,8 @@ on PostgreSQL 16+ `ADMIN OPTION` on `pg_monitor` and `pg_read_all_stats`. Otherw
|
|
|
264
284
|
printed and nothing is created.
|
|
265
285
|
|
|
266
286
|
The host and the port are the ones in the URL: `host` or `port` in the query string is refused.
|
|
267
|
-
Of the query string, the monitoring box gets `sslmode
|
|
287
|
+
Of the query string, the monitoring box gets `sslmode` and `application_name`; `channel_binding`
|
|
288
|
+
is removed using the collector rule above, before a price is quoted or anything is changed.
|
|
268
289
|
Certificate files (`sslrootcert`, `sslcert`, `sslkey`) are used for the connections from this
|
|
269
290
|
machine only; with `sslmode=verify-ca` or `verify-full` and a private CA in `sslrootcert`, `next`
|
|
270
291
|
says that the box has no copy of that CA.
|
|
@@ -506,7 +527,7 @@ Tools exposed:
|
|
|
506
527
|
- `update_issue_comment`: update an existing comment (args: `{ comment_id, org_id, content?, attachments?, debug? }`).
|
|
507
528
|
- `upload_file`: upload a local file and return the storage URL plus a ready-to-paste markdown link (args: `{ path, org_id, debug? }`).
|
|
508
529
|
- `download_file`: download a file from storage (args: `{ url, org_id, output_path?, debug? }`).
|
|
509
|
-
- `connect_database`: `pgai connect` as a tool (args: `{ database_url, org_id, provider?, clickhouse_key?, debug? }`). Returns the same JSON (`status`: `connected`, `provisioning`, `disconnecting`, `action_required` or `failed`; `dashboard_url`; `next`). It does not wait for the monitoring box: call it again to see the status. `database_url` must carry its password, and only the query parameters `sslmode`, `channel_binding`, `application_name` (and `password`); `PGAI_MON_PASSWORD`, `PGPASSWORD` and `CLICKHOUSE_KEY_ID` + `CLICKHOUSE_KEY_SECRET` of the server process are not used, a TLS failure is not retried in plaintext, and `--self-hosted` is CLI-only. For another database on a server PostgresAI already monitors for the organization, an admin `database_url` with `sslmode=require` (or `verify-*`) is enough: PostgresAI fills in the password of `postgres_ai_mon` it keeps.
|
|
530
|
+
- `connect_database`: `pgai connect` as a tool (args: `{ database_url, org_id, provider?, clickhouse_key?, yes?, coupon?, debug? }`). Returns the same JSON (`status`: `connected`, `provisioning`, `disconnecting`, `action_required` or `failed`; `dashboard_url`; `next`). For a billed box, the tool first returns the price with `action_required`; an explicit call with `yes: true` accepts it. `coupon` supplies a promotion code. It does not wait for the monitoring box: call it again to see the status. `database_url` must carry its password, and only the query parameters `sslmode`, `channel_binding`, `application_name` (and `password`); `PGAI_MON_PASSWORD`, `PGPASSWORD` and `CLICKHOUSE_KEY_ID` + `CLICKHOUSE_KEY_SECRET` of the server process are not used, a TLS failure is not retried in plaintext, and `--self-hosted` is CLI-only. For another database on a server PostgresAI already monitors for the organization, an admin `database_url` with `sslmode=require` (or `verify-*`) is enough: PostgresAI fills in the password of `postgres_ai_mon` it keeps.
|
|
510
531
|
|
|
511
532
|
#### `attachments` parameter (issue/comment tools)
|
|
512
533
|
|
package/dist/bin/postgres-ai.js
CHANGED
|
@@ -13533,7 +13533,7 @@ var {
|
|
|
13533
13533
|
// package.json
|
|
13534
13534
|
var package_default = {
|
|
13535
13535
|
name: "postgresai",
|
|
13536
|
-
version: "0.17.0-dev.
|
|
13536
|
+
version: "0.17.0-dev.9",
|
|
13537
13537
|
description: "postgres_ai CLI",
|
|
13538
13538
|
license: "Apache-2.0",
|
|
13539
13539
|
private: false,
|
|
@@ -16893,7 +16893,7 @@ var Result = import_lib.default.Result;
|
|
|
16893
16893
|
var TypeOverrides = import_lib.default.TypeOverrides;
|
|
16894
16894
|
var defaults = import_lib.default.defaults;
|
|
16895
16895
|
// package.json
|
|
16896
|
-
var version = "0.17.0-dev.
|
|
16896
|
+
var version = "0.17.0-dev.9";
|
|
16897
16897
|
var package_default2 = {
|
|
16898
16898
|
name: "postgresai",
|
|
16899
16899
|
version,
|
|
@@ -18501,6 +18501,13 @@ var Result2 = import_lib2.default.Result;
|
|
|
18501
18501
|
var TypeOverrides2 = import_lib2.default.TypeOverrides;
|
|
18502
18502
|
var defaults2 = import_lib2.default.defaults;
|
|
18503
18503
|
|
|
18504
|
+
// node_modules/pg-connection-string/esm/index.mjs
|
|
18505
|
+
var import___ = __toESM(require_pg_connection_string(), 1);
|
|
18506
|
+
var esm_default = import___.default.parse;
|
|
18507
|
+
var parse = import___.default.parse;
|
|
18508
|
+
var toClientConfig = import___.default.toClientConfig;
|
|
18509
|
+
var parseIntoClientConfig = import___.default.parseIntoClientConfig;
|
|
18510
|
+
|
|
18504
18511
|
// lib/checkup.ts
|
|
18505
18512
|
import * as fs4 from "fs";
|
|
18506
18513
|
import * as path4 from "path";
|
|
@@ -22577,12 +22584,13 @@ async function generateD004(client, nodeName) {
|
|
|
22577
22584
|
let pgssError = null;
|
|
22578
22585
|
const pgssSampleQueries = [];
|
|
22579
22586
|
try {
|
|
22580
|
-
const extCheck = await client.query("select
|
|
22587
|
+
const extCheck = await client.query("select n.nspname as schema from pg_catalog.pg_extension e join pg_catalog.pg_namespace n on n.oid = e.extnamespace where e.extname = 'pg_stat_statements'");
|
|
22581
22588
|
if (extCheck.rows.length > 0) {
|
|
22582
22589
|
pgssAvailable = true;
|
|
22590
|
+
const pgssView = quoteIdent(extCheck.rows[0].schema, "pg_stat_statements");
|
|
22583
22591
|
const statsResult = await client.query(`
|
|
22584
22592
|
select count(*) as cnt, coalesce(sum(calls), 0) as total_calls
|
|
22585
|
-
from
|
|
22593
|
+
from ${pgssView}
|
|
22586
22594
|
`);
|
|
22587
22595
|
pgssMetricsCount = parseInt(statsResult.rows[0]?.cnt || "0", 10);
|
|
22588
22596
|
pgssTotalCalls = parseInt(statsResult.rows[0]?.total_calls || "0", 10);
|
|
@@ -22592,7 +22600,7 @@ async function generateD004(client, nodeName) {
|
|
|
22592
22600
|
coalesce(usename, 'unknown') as "user",
|
|
22593
22601
|
coalesce(datname, 'unknown') as database,
|
|
22594
22602
|
calls
|
|
22595
|
-
from
|
|
22603
|
+
from ${pgssView} s
|
|
22596
22604
|
left join pg_database d on s.dbid = d.oid
|
|
22597
22605
|
left join pg_user u on s.userid = u.usesysid
|
|
22598
22606
|
order by calls desc
|
|
@@ -22620,16 +22628,17 @@ async function generateD004(client, nodeName) {
|
|
|
22620
22628
|
let kcacheError = null;
|
|
22621
22629
|
const kcacheSampleQueries = [];
|
|
22622
22630
|
try {
|
|
22623
|
-
const extCheck = await client.query("select
|
|
22631
|
+
const extCheck = await client.query("select n.nspname as schema from pg_catalog.pg_extension e join pg_catalog.pg_namespace n on n.oid = e.extnamespace where e.extname = 'pg_stat_kcache'");
|
|
22624
22632
|
if (extCheck.rows.length > 0) {
|
|
22625
22633
|
kcacheAvailable = true;
|
|
22634
|
+
const kcacheView = quoteIdent(extCheck.rows[0].schema, "pg_stat_kcache");
|
|
22626
22635
|
const statsResult = await client.query(`
|
|
22627
22636
|
select
|
|
22628
22637
|
count(*) as cnt,
|
|
22629
22638
|
coalesce(sum(exec_user_time + exec_system_time), 0) as total_exec_time,
|
|
22630
22639
|
coalesce(sum(exec_user_time), 0) as total_user_time,
|
|
22631
22640
|
coalesce(sum(exec_system_time), 0) as total_system_time
|
|
22632
|
-
from
|
|
22641
|
+
from ${kcacheView}
|
|
22633
22642
|
`);
|
|
22634
22643
|
kcacheMetricsCount = parseInt(statsResult.rows[0]?.cnt || "0", 10);
|
|
22635
22644
|
kcacheTotalExecTime = parseFloat(statsResult.rows[0]?.total_exec_time || "0");
|
|
@@ -22640,7 +22649,7 @@ async function generateD004(client, nodeName) {
|
|
|
22640
22649
|
queryid::text as queryid,
|
|
22641
22650
|
coalesce(usename, 'unknown') as "user",
|
|
22642
22651
|
(exec_user_time + exec_system_time) as exec_total_time
|
|
22643
|
-
from
|
|
22652
|
+
from ${kcacheView} k
|
|
22644
22653
|
left join pg_user u on k.userid = u.usesysid
|
|
22645
22654
|
order by (exec_user_time + exec_system_time) desc
|
|
22646
22655
|
limit 5
|
|
@@ -27216,15 +27225,6 @@ import * as path5 from "path";
|
|
|
27216
27225
|
|
|
27217
27226
|
// lib/instances.ts
|
|
27218
27227
|
import * as fs5 from "fs";
|
|
27219
|
-
|
|
27220
|
-
// node_modules/pg-connection-string/esm/index.mjs
|
|
27221
|
-
var import___ = __toESM(require_pg_connection_string(), 1);
|
|
27222
|
-
var esm_default = import___.default.parse;
|
|
27223
|
-
var parse = import___.default.parse;
|
|
27224
|
-
var toClientConfig = import___.default.toClientConfig;
|
|
27225
|
-
var parseIntoClientConfig = import___.default.parseIntoClientConfig;
|
|
27226
|
-
|
|
27227
|
-
// lib/instances.ts
|
|
27228
27228
|
class InstancesParseError extends Error {
|
|
27229
27229
|
constructor(file, cause) {
|
|
27230
27230
|
const yamlErr = cause;
|
|
@@ -27254,7 +27254,68 @@ function loadInstances(file) {
|
|
|
27254
27254
|
}
|
|
27255
27255
|
return parsed;
|
|
27256
27256
|
}
|
|
27257
|
+
function extractSslmode(connStr) {
|
|
27258
|
+
try {
|
|
27259
|
+
return (new URL(connStr).searchParams.get("sslmode") || "").toLowerCase();
|
|
27260
|
+
} catch {
|
|
27261
|
+
return "";
|
|
27262
|
+
}
|
|
27263
|
+
}
|
|
27264
|
+
function sslOptionFromSslmode(sslmode) {
|
|
27265
|
+
switch (sslmode) {
|
|
27266
|
+
case "disable":
|
|
27267
|
+
return false;
|
|
27268
|
+
case "verify-ca":
|
|
27269
|
+
return { rejectUnauthorized: true, checkServerIdentity: () => {
|
|
27270
|
+
return;
|
|
27271
|
+
} };
|
|
27272
|
+
case "verify-full":
|
|
27273
|
+
return { rejectUnauthorized: true };
|
|
27274
|
+
case "allow":
|
|
27275
|
+
case "prefer":
|
|
27276
|
+
case "require":
|
|
27277
|
+
case "no-verify":
|
|
27278
|
+
case "":
|
|
27279
|
+
default:
|
|
27280
|
+
return { rejectUnauthorized: false };
|
|
27281
|
+
}
|
|
27282
|
+
}
|
|
27257
27283
|
var LAX_SSLMODES = new Set(["", "allow", "prefer", "require"]);
|
|
27284
|
+
function buildClientConfig(connStr, extra = {}) {
|
|
27285
|
+
const sslmode = extractSslmode(connStr);
|
|
27286
|
+
const channelBinding = splitChannelBinding(connStr);
|
|
27287
|
+
const enableChannelBinding = requireChannelBinding(channelBinding.value, sslmode === "disable");
|
|
27288
|
+
const parsed = parse(withoutSslmode(channelBinding.uri));
|
|
27289
|
+
return {
|
|
27290
|
+
host: parsed.host || undefined,
|
|
27291
|
+
port: parsed.port ? Number(parsed.port) : undefined,
|
|
27292
|
+
user: parsed.user,
|
|
27293
|
+
password: parsed.password,
|
|
27294
|
+
database: databaseName(withoutSslmode(channelBinding.uri), parsed.database),
|
|
27295
|
+
ssl: sslOptionFromSslmode(sslmode),
|
|
27296
|
+
...enableChannelBinding ? { enableChannelBinding: true } : {},
|
|
27297
|
+
...extra
|
|
27298
|
+
};
|
|
27299
|
+
}
|
|
27300
|
+
function databaseName(connStr, parsed) {
|
|
27301
|
+
if (!parsed)
|
|
27302
|
+
return;
|
|
27303
|
+
try {
|
|
27304
|
+
const u = new URL(connStr);
|
|
27305
|
+
if (/^postgres(ql)?:$/.test(u.protocol))
|
|
27306
|
+
return decodeURIComponent(u.pathname.slice(1));
|
|
27307
|
+
} catch {}
|
|
27308
|
+
return parsed;
|
|
27309
|
+
}
|
|
27310
|
+
function withoutSslmode(connStr) {
|
|
27311
|
+
try {
|
|
27312
|
+
const u = new URL(connStr);
|
|
27313
|
+
u.searchParams.delete("sslmode");
|
|
27314
|
+
return u.toString();
|
|
27315
|
+
} catch {
|
|
27316
|
+
return connStr;
|
|
27317
|
+
}
|
|
27318
|
+
}
|
|
27258
27319
|
function splitChannelBinding(uri) {
|
|
27259
27320
|
let value = null;
|
|
27260
27321
|
try {
|
|
@@ -27268,6 +27329,47 @@ function splitChannelBinding(uri) {
|
|
|
27268
27329
|
const params = uri.slice(start + 1, end).split("&").filter((param) => !new URLSearchParams(param).has("channel_binding"));
|
|
27269
27330
|
return { uri: uri.slice(0, start) + (params.length ? `?${params.join("&")}` : "") + uri.slice(end), value };
|
|
27270
27331
|
}
|
|
27332
|
+
async function verifyCollectorTls(url) {
|
|
27333
|
+
const u = new URL(url);
|
|
27334
|
+
const ca = u.searchParams.get("sslrootcert");
|
|
27335
|
+
for (const key of ["sslrootcert", "sslcert", "sslkey", "uselibpqcompat"])
|
|
27336
|
+
u.searchParams.delete(key);
|
|
27337
|
+
const client = new Client2({
|
|
27338
|
+
...buildClientConfig(splitChannelBinding(u.toString()).uri, { connectionTimeoutMillis: 1e4 }),
|
|
27339
|
+
ssl: { rejectUnauthorized: true, servername: u.hostname, ...ca ? { ca: fs5.readFileSync(ca, "utf8") } : {} }
|
|
27340
|
+
});
|
|
27341
|
+
try {
|
|
27342
|
+
await client.connect();
|
|
27343
|
+
} catch (err) {
|
|
27344
|
+
if (!client.connection.stream.authorized)
|
|
27345
|
+
throw err;
|
|
27346
|
+
} finally {
|
|
27347
|
+
await client.end().catch(() => {});
|
|
27348
|
+
}
|
|
27349
|
+
}
|
|
27350
|
+
async function collectorConnection(url, verifyTls = verifyCollectorTls) {
|
|
27351
|
+
const binding = splitChannelBinding(url);
|
|
27352
|
+
const sslmode = extractSslmode(url);
|
|
27353
|
+
if (binding.value === "require") {
|
|
27354
|
+
if (sslmode === "disable" || sslmode === "allow") {
|
|
27355
|
+
throw new Error(`channel_binding=require needs TLS, but sslmode=${sslmode} is set`);
|
|
27356
|
+
}
|
|
27357
|
+
let collectorUrl = binding.uri;
|
|
27358
|
+
if (sslmode !== "verify-full") {
|
|
27359
|
+
const u = new URL(collectorUrl);
|
|
27360
|
+
u.searchParams.set("sslmode", "verify-full");
|
|
27361
|
+
collectorUrl = u.toString();
|
|
27362
|
+
try {
|
|
27363
|
+
await verifyTls(collectorUrl);
|
|
27364
|
+
} catch (err) {
|
|
27365
|
+
const why = err instanceof Error && err.message ? ` (${err.message})` : "";
|
|
27366
|
+
throw new Error(`Could not verify the TLS certificate for channel_binding=require${why}. Provide sslrootcert=<CA file> with sslmode=verify-full, or explicitly accept the downgrade by removing channel_binding=require from the URL`);
|
|
27367
|
+
}
|
|
27368
|
+
}
|
|
27369
|
+
return { url: collectorUrl, note: "the collector can't do channel binding; it connects with TLS and full certificate verification" + (sslmode !== "verify-full" ? "; upgraded to sslmode=verify-full" : "") };
|
|
27370
|
+
}
|
|
27371
|
+
return { url: binding.uri, ...binding.value !== null ? { note: "removed channel_binding from the connection string; the collector does not support it (TLS is kept)" } : {} };
|
|
27372
|
+
}
|
|
27271
27373
|
function requireChannelBinding(value, sslDisabled) {
|
|
27272
27374
|
if (value !== "require")
|
|
27273
27375
|
return false;
|
|
@@ -28005,7 +28107,7 @@ function detectCloudProvider(url) {
|
|
|
28005
28107
|
return "supabase";
|
|
28006
28108
|
return "self-managed";
|
|
28007
28109
|
}
|
|
28008
|
-
function
|
|
28110
|
+
function databaseName2(url) {
|
|
28009
28111
|
const u = new URL(url);
|
|
28010
28112
|
const port = u.port && u.port !== "5432" ? `:${u.port}` : "";
|
|
28011
28113
|
let db = decodeURIComponent(u.pathname.replace(/^\//, ""));
|
|
@@ -28046,15 +28148,33 @@ function clusterOf(url) {
|
|
|
28046
28148
|
return u?.hostname ? `${u.hostname.toLowerCase()}:${u.port || "5432"}` : undefined;
|
|
28047
28149
|
}
|
|
28048
28150
|
var errorText = (err) => err instanceof AggregateError && !err.message ? err.errors.map(errorText).join("; ") : err instanceof Error ? err.message : String(err);
|
|
28049
|
-
var urlPassword = (u) =>
|
|
28151
|
+
var urlPassword = (u) => {
|
|
28152
|
+
for (const key of [...u.searchParams.keys()])
|
|
28153
|
+
if (key !== "password")
|
|
28154
|
+
u.searchParams.delete(key);
|
|
28155
|
+
return parse(u.toString()).password || "";
|
|
28156
|
+
};
|
|
28050
28157
|
var URL_PARAMS_KEPT = ["sslmode", "channel_binding", "application_name"];
|
|
28051
28158
|
var URL_PARAMS_TLS = ["sslrootcert", "sslcert", "sslkey", "uselibpqcompat"];
|
|
28052
28159
|
function checkUrlParams(url, agent) {
|
|
28053
|
-
const
|
|
28160
|
+
const u = new URL(url);
|
|
28161
|
+
const keys = [...new Set(u.searchParams.keys())];
|
|
28054
28162
|
if (agent) {
|
|
28163
|
+
for (const key of ["password", "user", "host", "port", "dbname"]) {
|
|
28164
|
+
if (u.searchParams.getAll(key).length > 1)
|
|
28165
|
+
throw new Error(`database_url query parameter ${key} must appear only once`);
|
|
28166
|
+
}
|
|
28167
|
+
if (u.password && u.searchParams.has("password"))
|
|
28168
|
+
throw new Error("database_url must give the password only once, in the authority or in ?password=");
|
|
28055
28169
|
const refused = keys.filter((k) => k !== "password" && !URL_PARAMS_KEPT.includes(k));
|
|
28056
28170
|
if (refused.length)
|
|
28057
28171
|
throw new Error(`database_url may carry only these query parameters: ${URL_PARAMS_KEPT.join(", ")} (got: ${refused.join(", ")})`);
|
|
28172
|
+
u.searchParams.delete("sslmode");
|
|
28173
|
+
u.searchParams.delete("channel_binding");
|
|
28174
|
+
const config = parseIntoClientConfig(u.toString());
|
|
28175
|
+
if (!/^postgres(ql)?:$/.test(u.protocol) || typeof config.password !== "string" || !config.password)
|
|
28176
|
+
throw new Error("database_url must be postgresql://user:password@host:5432/dbname, with the password in it");
|
|
28177
|
+
return { ...config, password: config.password };
|
|
28058
28178
|
}
|
|
28059
28179
|
const moved = keys.filter((k) => k === "host" || k === "port");
|
|
28060
28180
|
if (moved.length)
|
|
@@ -28087,7 +28207,7 @@ function roleUrlFor(url, db, password, kept) {
|
|
|
28087
28207
|
u.search = u.search.replace(/@/g, "%40");
|
|
28088
28208
|
return u.toString();
|
|
28089
28209
|
}
|
|
28090
|
-
var monitoringUrlFor = (url, db, password) => roleUrlFor(url, db, password, URL_PARAMS_KEPT);
|
|
28210
|
+
var monitoringUrlFor = (url, db, password) => roleUrlFor(url, db, password, URL_PARAMS_KEPT.filter((p) => p !== "channel_binding"));
|
|
28091
28211
|
var loginUrlFor = (url, db, password) => roleUrlFor(url, db, password, [...URL_PARAMS_KEPT, ...URL_PARAMS_TLS]);
|
|
28092
28212
|
function caNote(url) {
|
|
28093
28213
|
const q = new URL(url).searchParams;
|
|
@@ -28163,7 +28283,19 @@ async function connect(url, opts, deps) {
|
|
|
28163
28283
|
if (!PROVIDERS.includes(provider))
|
|
28164
28284
|
throw new Error(`--provider must be one of: ${PROVIDERS.join(", ")}`);
|
|
28165
28285
|
checkUrlParams(url, opts.agent);
|
|
28166
|
-
const name =
|
|
28286
|
+
const name = databaseName2(url);
|
|
28287
|
+
try {
|
|
28288
|
+
const collector = await collectorConnection(url, deps.verifyTls);
|
|
28289
|
+
const required = splitChannelBinding(url).value === "require";
|
|
28290
|
+
url = collector.url;
|
|
28291
|
+
if (collector.note)
|
|
28292
|
+
progress("preparing", `${required ? "Warning" : "Note"}: ${collector.note}`);
|
|
28293
|
+
if (required && caNote(url))
|
|
28294
|
+
progress("preparing", `Note: ${caNote(url)}`);
|
|
28295
|
+
} catch (err) {
|
|
28296
|
+
return { status: "action_required", provider, name, next: err instanceof Error ? err.message : String(err) };
|
|
28297
|
+
}
|
|
28298
|
+
const cluster = clusterOf(url);
|
|
28167
28299
|
const key = parseClickhouseKey(opts.clickhouseKey, provider === "clickhouse" && !opts.agent ? process.env : {});
|
|
28168
28300
|
if (key && provider !== "clickhouse")
|
|
28169
28301
|
throw new Error("--clickhouse-key applies to ClickHouse Managed Postgres only");
|
|
@@ -28176,8 +28308,12 @@ async function connect(url, opts, deps) {
|
|
|
28176
28308
|
if (opts.selfHosted && deps.localStackRunning()) {
|
|
28177
28309
|
return { status: "action_required", provider, name, next: "A monitoring stack already runs on this machine: add the database with PGAI_DB_URL='<postgres_ai_mon URL>' pgai mon targets add" };
|
|
28178
28310
|
}
|
|
28311
|
+
const findDatabase = (list) => {
|
|
28312
|
+
const live = list.filter((d) => !disconnecting(d.status));
|
|
28313
|
+
return live.find((d) => d.cluster === cluster && d.database != null && decodeURIComponent(d.database) === name.split("/").slice(1).join("/")) ?? live.find((d) => (d.cluster == null || d.database == null) && d.name === name);
|
|
28314
|
+
};
|
|
28179
28315
|
const rows = opts.selfHosted ? [] : await deps.list();
|
|
28180
|
-
let row = rows
|
|
28316
|
+
let row = findDatabase(rows);
|
|
28181
28317
|
const fresh = !row;
|
|
28182
28318
|
let note = "";
|
|
28183
28319
|
let checkup;
|
|
@@ -28219,9 +28355,10 @@ async function connect(url, opts, deps) {
|
|
|
28219
28355
|
await deps.unprepare(url).catch(() => false);
|
|
28220
28356
|
};
|
|
28221
28357
|
let lockId;
|
|
28222
|
-
const unread = (d) =>
|
|
28223
|
-
const
|
|
28224
|
-
const
|
|
28358
|
+
const unread = (d) => !d.cluster && !/^[A-Za-z0-9._\[\]:-]{1,253}(?::[0-9]+)?(?:\/[A-Za-z0-9._-]*)?$/.test(d.name);
|
|
28359
|
+
const onServer = (d) => d.cluster ? d.cluster === cluster : !unread(d) && serverOf(d.name) === serverOf(name);
|
|
28360
|
+
const othersOnServer = (list) => list.filter((d) => !disconnecting(d.status) && (unread(d) || onServer(d)));
|
|
28361
|
+
const storedPassword = rows.some((d) => d.monitoring_password_stored && !/delet/.test(d.status ?? "") && onServer(d)) || undefined;
|
|
28225
28362
|
const cutOff = (others) => {
|
|
28226
28363
|
const on = others.filter((d) => !unread(d)).map((d) => d.name).join(", ");
|
|
28227
28364
|
const maybe = others.filter(unread).map((d) => d.name).join(", ");
|
|
@@ -28242,8 +28379,13 @@ async function connect(url, opts, deps) {
|
|
|
28242
28379
|
const prepOpts = { resetPassword: opts.resetPassword, ...storedPassword ? { storedPassword, others } : {} };
|
|
28243
28380
|
if (!opts.selfHosted) {
|
|
28244
28381
|
const probe = await deps.prepare(url, provider, { ...prepOpts, check: true });
|
|
28245
|
-
if ("next" in probe)
|
|
28246
|
-
|
|
28382
|
+
if ("next" in probe) {
|
|
28383
|
+
const { resettable, ...refusal } = probe;
|
|
28384
|
+
if (resettable && !opts.resetPassword && !opts.agent && await deps.confirm("A monitoring role from an earlier connection exists, and its password isn't stored. Reset it now? Anything else using this role will need the new password. [y/N] ")) {
|
|
28385
|
+
return connect(url, { ...opts, resetPassword: true }, deps);
|
|
28386
|
+
}
|
|
28387
|
+
return { status: "action_required", provider, name, ...refusal };
|
|
28388
|
+
}
|
|
28247
28389
|
const b = await billingFor(await deps.quote(opts.coupon, clusterOf(url)), name, opts, deps, (price) => progress("billing", `Billing: ${price}`));
|
|
28248
28390
|
if (b.stop)
|
|
28249
28391
|
return { status: "action_required", provider, name, ...b.stop };
|
|
@@ -28252,10 +28394,10 @@ async function connect(url, opts, deps) {
|
|
|
28252
28394
|
if (opts.resetPassword) {
|
|
28253
28395
|
let lock;
|
|
28254
28396
|
try {
|
|
28255
|
-
lock = await deps.resetLock(
|
|
28397
|
+
lock = await deps.resetLock(cluster);
|
|
28256
28398
|
} catch (err) {
|
|
28257
28399
|
if (err instanceof HttpStatusError && err.status === 409) {
|
|
28258
|
-
return { status: "action_required", provider, name, ...billing, next: `Another pgai connect --reset-password for ${
|
|
28400
|
+
return { status: "action_required", provider, name, ...billing, next: `Another pgai connect --reset-password for ${cluster} is running: wait for it to finish, then re-run (a run that stopped frees the server 15 minutes after it started)` };
|
|
28259
28401
|
}
|
|
28260
28402
|
if (err instanceof HttpStatusError && err.status === 404) {
|
|
28261
28403
|
return { status: "action_required", provider, name, ...billing, next: `This platform cannot lock the server for --reset-password yet: set PGAI_MON_PASSWORD to ${DEFAULT_MONITORING_USER}'s password instead` };
|
|
@@ -28272,7 +28414,7 @@ async function connect(url, opts, deps) {
|
|
|
28272
28414
|
try {
|
|
28273
28415
|
if (lockId) {
|
|
28274
28416
|
const now = await deps.list();
|
|
28275
|
-
const same = now
|
|
28417
|
+
const same = findDatabase(now);
|
|
28276
28418
|
if (same)
|
|
28277
28419
|
return { status: "action_required", provider, name, id: same.id, ...billing, next: `${name} is already connected, and its monitoring uses the current password: pgai disconnect ${name} --yes first` };
|
|
28278
28420
|
const others = othersOnServer(now);
|
|
@@ -28281,8 +28423,10 @@ async function connect(url, opts, deps) {
|
|
|
28281
28423
|
}
|
|
28282
28424
|
progress("preparing", `Preparing ${maskConnectionString(url)}`);
|
|
28283
28425
|
prepared = await deps.prepare(url, provider, prepOpts);
|
|
28284
|
-
if ("next" in prepared)
|
|
28285
|
-
|
|
28426
|
+
if ("next" in prepared) {
|
|
28427
|
+
const { resettable, ...refusal } = prepared;
|
|
28428
|
+
return { status: "action_required", provider, name, ...billing, ...refusal };
|
|
28429
|
+
}
|
|
28286
28430
|
if (!("monitoringUrl" in prepared))
|
|
28287
28431
|
throw new Error("prepare returned no monitoring URL");
|
|
28288
28432
|
if (prepared.note)
|
|
@@ -28343,8 +28487,9 @@ async function connect(url, opts, deps) {
|
|
|
28343
28487
|
}
|
|
28344
28488
|
let shown;
|
|
28345
28489
|
for (const deadline = deps.now() + opts.waitMs;; ) {
|
|
28346
|
-
if (
|
|
28347
|
-
|
|
28490
|
+
if (row.billing_error && disconnecting(row.status)) {
|
|
28491
|
+
if (undoRole)
|
|
28492
|
+
await undoRole();
|
|
28348
28493
|
return {
|
|
28349
28494
|
status: "action_required",
|
|
28350
28495
|
provider,
|
|
@@ -28352,7 +28497,7 @@ async function connect(url, opts, deps) {
|
|
|
28352
28497
|
id: row.id,
|
|
28353
28498
|
...billing,
|
|
28354
28499
|
...checkup ? { checkup } : {},
|
|
28355
|
-
next: `The first charge failed (${row.billing_error}): the box
|
|
28500
|
+
next: `The first charge failed (${row.billing_error}): the box is being removed and nothing is billed. Update the payment method${billingUrl ? ` at ${billingUrl}` : ""}, then re-run`
|
|
28356
28501
|
};
|
|
28357
28502
|
}
|
|
28358
28503
|
const result = connectStatus(row, provider, fresh);
|
|
@@ -28380,9 +28525,15 @@ async function connect(url, opts, deps) {
|
|
|
28380
28525
|
}
|
|
28381
28526
|
}
|
|
28382
28527
|
async function openConnection(url, opts) {
|
|
28528
|
+
const config = checkUrlParams(url, opts.agent);
|
|
28383
28529
|
const conn = resolveAdminConnection({ conn: url });
|
|
28530
|
+
if (config) {
|
|
28531
|
+
const { connectionString, ...explicit } = conn.clientConfig;
|
|
28532
|
+
conn.clientConfig = { ...config, ...explicit };
|
|
28533
|
+
}
|
|
28384
28534
|
const fallback = !opts.agent && !!conn.sslFallbackEnabled;
|
|
28385
28535
|
const { client, usedSsl } = await connectWithSslFallback(opts.Client ?? Client2, { ...conn, sslFallbackEnabled: fallback });
|
|
28536
|
+
await client.query("set search_path = pg_catalog, pg_temp");
|
|
28386
28537
|
return { client, usedSsl, refusedTls: fallback && !usedSsl };
|
|
28387
28538
|
}
|
|
28388
28539
|
async function dropMonitoringRole(client) {
|
|
@@ -28483,7 +28634,7 @@ async function prepareDatabase(url, provider, opts = {}) {
|
|
|
28483
28634
|
const reset = !!opts.resetPassword && me.mon_exists;
|
|
28484
28635
|
if (me.mon_exists && !reset) {
|
|
28485
28636
|
if (!process.env.PGAI_MON_PASSWORD?.trim())
|
|
28486
|
-
return { next: `${exists}. ${setPassword}
|
|
28637
|
+
return { next: `${exists}. ${setPassword}`, resettable: true };
|
|
28487
28638
|
let accepted;
|
|
28488
28639
|
try {
|
|
28489
28640
|
accepted = await logsIn(loginUrlFor(url, me.db, password));
|
|
@@ -28596,6 +28747,7 @@ async function clickhouseOrgFor(host, keyId, keySecret) {
|
|
|
28596
28747
|
function platformDeps(p) {
|
|
28597
28748
|
const rpc = (fn, body = {}) => callRpc({ apiKey: p.apiKey, apiBaseUrl: p.apiBaseUrl, fn, body, operation: fn.replace(/_/g, " "), debug: p.debug, orgScope: p.orgScope });
|
|
28598
28749
|
return {
|
|
28750
|
+
verifyTls: verifyCollectorTls,
|
|
28599
28751
|
list: () => rpc("cloud_monitoring_list"),
|
|
28600
28752
|
create: (body) => rpc("cloud_monitoring_connect", body),
|
|
28601
28753
|
disconnect: (id) => rpc("cloud_monitoring_disconnect", { instance_id: id }),
|
|
@@ -35294,7 +35446,7 @@ async function handleToolCall(req, rootOpts, extra) {
|
|
|
35294
35446
|
return scope.error;
|
|
35295
35447
|
const url = String(args.database_url ?? "");
|
|
35296
35448
|
const u = parseUrl(url);
|
|
35297
|
-
if (!u || !/^postgres(ql)?:$/.test(u.protocol)
|
|
35449
|
+
if (!u || !/^postgres(ql)?:$/.test(u.protocol)) {
|
|
35298
35450
|
throw new Error("database_url must be postgresql://user:password@host:5432/dbname, with the password in it");
|
|
35299
35451
|
}
|
|
35300
35452
|
const result = await connect(url, {
|
|
@@ -36827,7 +36979,7 @@ function detectCloudProvider2(url) {
|
|
|
36827
36979
|
return "supabase";
|
|
36828
36980
|
return "self-managed";
|
|
36829
36981
|
}
|
|
36830
|
-
function
|
|
36982
|
+
function databaseName3(url) {
|
|
36831
36983
|
const u = new URL(url);
|
|
36832
36984
|
const port = u.port && u.port !== "5432" ? `:${u.port}` : "";
|
|
36833
36985
|
let db = decodeURIComponent(u.pathname.replace(/^\//, ""));
|
|
@@ -36868,15 +37020,33 @@ function clusterOf2(url) {
|
|
|
36868
37020
|
return u?.hostname ? `${u.hostname.toLowerCase()}:${u.port || "5432"}` : undefined;
|
|
36869
37021
|
}
|
|
36870
37022
|
var errorText2 = (err) => err instanceof AggregateError && !err.message ? err.errors.map(errorText2).join("; ") : err instanceof Error ? err.message : String(err);
|
|
36871
|
-
var urlPassword2 = (u) =>
|
|
37023
|
+
var urlPassword2 = (u) => {
|
|
37024
|
+
for (const key of [...u.searchParams.keys()])
|
|
37025
|
+
if (key !== "password")
|
|
37026
|
+
u.searchParams.delete(key);
|
|
37027
|
+
return parse(u.toString()).password || "";
|
|
37028
|
+
};
|
|
36872
37029
|
var URL_PARAMS_KEPT2 = ["sslmode", "channel_binding", "application_name"];
|
|
36873
37030
|
var URL_PARAMS_TLS2 = ["sslrootcert", "sslcert", "sslkey", "uselibpqcompat"];
|
|
36874
37031
|
function checkUrlParams2(url, agent) {
|
|
36875
|
-
const
|
|
37032
|
+
const u = new URL(url);
|
|
37033
|
+
const keys = [...new Set(u.searchParams.keys())];
|
|
36876
37034
|
if (agent) {
|
|
37035
|
+
for (const key of ["password", "user", "host", "port", "dbname"]) {
|
|
37036
|
+
if (u.searchParams.getAll(key).length > 1)
|
|
37037
|
+
throw new Error(`database_url query parameter ${key} must appear only once`);
|
|
37038
|
+
}
|
|
37039
|
+
if (u.password && u.searchParams.has("password"))
|
|
37040
|
+
throw new Error("database_url must give the password only once, in the authority or in ?password=");
|
|
36877
37041
|
const refused = keys.filter((k) => k !== "password" && !URL_PARAMS_KEPT2.includes(k));
|
|
36878
37042
|
if (refused.length)
|
|
36879
37043
|
throw new Error(`database_url may carry only these query parameters: ${URL_PARAMS_KEPT2.join(", ")} (got: ${refused.join(", ")})`);
|
|
37044
|
+
u.searchParams.delete("sslmode");
|
|
37045
|
+
u.searchParams.delete("channel_binding");
|
|
37046
|
+
const config = parseIntoClientConfig(u.toString());
|
|
37047
|
+
if (!/^postgres(ql)?:$/.test(u.protocol) || typeof config.password !== "string" || !config.password)
|
|
37048
|
+
throw new Error("database_url must be postgresql://user:password@host:5432/dbname, with the password in it");
|
|
37049
|
+
return { ...config, password: config.password };
|
|
36880
37050
|
}
|
|
36881
37051
|
const moved = keys.filter((k) => k === "host" || k === "port");
|
|
36882
37052
|
if (moved.length)
|
|
@@ -36909,7 +37079,7 @@ function roleUrlFor2(url, db, password, kept) {
|
|
|
36909
37079
|
u.search = u.search.replace(/@/g, "%40");
|
|
36910
37080
|
return u.toString();
|
|
36911
37081
|
}
|
|
36912
|
-
var monitoringUrlFor2 = (url, db, password) => roleUrlFor2(url, db, password, URL_PARAMS_KEPT2);
|
|
37082
|
+
var monitoringUrlFor2 = (url, db, password) => roleUrlFor2(url, db, password, URL_PARAMS_KEPT2.filter((p) => p !== "channel_binding"));
|
|
36913
37083
|
var loginUrlFor2 = (url, db, password) => roleUrlFor2(url, db, password, [...URL_PARAMS_KEPT2, ...URL_PARAMS_TLS2]);
|
|
36914
37084
|
function caNote2(url) {
|
|
36915
37085
|
const q = new URL(url).searchParams;
|
|
@@ -36994,7 +37164,19 @@ async function connect2(url, opts, deps) {
|
|
|
36994
37164
|
if (!PROVIDERS2.includes(provider))
|
|
36995
37165
|
throw new Error(`--provider must be one of: ${PROVIDERS2.join(", ")}`);
|
|
36996
37166
|
checkUrlParams2(url, opts.agent);
|
|
36997
|
-
const name =
|
|
37167
|
+
const name = databaseName3(url);
|
|
37168
|
+
try {
|
|
37169
|
+
const collector = await collectorConnection(url, deps.verifyTls);
|
|
37170
|
+
const required = splitChannelBinding(url).value === "require";
|
|
37171
|
+
url = collector.url;
|
|
37172
|
+
if (collector.note)
|
|
37173
|
+
progress("preparing", `${required ? "Warning" : "Note"}: ${collector.note}`);
|
|
37174
|
+
if (required && caNote2(url))
|
|
37175
|
+
progress("preparing", `Note: ${caNote2(url)}`);
|
|
37176
|
+
} catch (err) {
|
|
37177
|
+
return { status: "action_required", provider, name, next: err instanceof Error ? err.message : String(err) };
|
|
37178
|
+
}
|
|
37179
|
+
const cluster = clusterOf2(url);
|
|
36998
37180
|
const key = parseClickhouseKey2(opts.clickhouseKey, provider === "clickhouse" && !opts.agent ? process.env : {});
|
|
36999
37181
|
if (key && provider !== "clickhouse")
|
|
37000
37182
|
throw new Error("--clickhouse-key applies to ClickHouse Managed Postgres only");
|
|
@@ -37007,8 +37189,12 @@ async function connect2(url, opts, deps) {
|
|
|
37007
37189
|
if (opts.selfHosted && deps.localStackRunning()) {
|
|
37008
37190
|
return { status: "action_required", provider, name, next: "A monitoring stack already runs on this machine: add the database with PGAI_DB_URL='<postgres_ai_mon URL>' pgai mon targets add" };
|
|
37009
37191
|
}
|
|
37192
|
+
const findDatabase = (list) => {
|
|
37193
|
+
const live = list.filter((d) => !disconnecting2(d.status));
|
|
37194
|
+
return live.find((d) => d.cluster === cluster && d.database != null && decodeURIComponent(d.database) === name.split("/").slice(1).join("/")) ?? live.find((d) => (d.cluster == null || d.database == null) && d.name === name);
|
|
37195
|
+
};
|
|
37010
37196
|
const rows = opts.selfHosted ? [] : await deps.list();
|
|
37011
|
-
let row = rows
|
|
37197
|
+
let row = findDatabase(rows);
|
|
37012
37198
|
const fresh = !row;
|
|
37013
37199
|
let note = "";
|
|
37014
37200
|
let checkup;
|
|
@@ -37050,9 +37236,10 @@ async function connect2(url, opts, deps) {
|
|
|
37050
37236
|
await deps.unprepare(url).catch(() => false);
|
|
37051
37237
|
};
|
|
37052
37238
|
let lockId;
|
|
37053
|
-
const unread = (d) =>
|
|
37054
|
-
const
|
|
37055
|
-
const
|
|
37239
|
+
const unread = (d) => !d.cluster && !/^[A-Za-z0-9._\[\]:-]{1,253}(?::[0-9]+)?(?:\/[A-Za-z0-9._-]*)?$/.test(d.name);
|
|
37240
|
+
const onServer = (d) => d.cluster ? d.cluster === cluster : !unread(d) && serverOf2(d.name) === serverOf2(name);
|
|
37241
|
+
const othersOnServer = (list) => list.filter((d) => !disconnecting2(d.status) && (unread(d) || onServer(d)));
|
|
37242
|
+
const storedPassword = rows.some((d) => d.monitoring_password_stored && !/delet/.test(d.status ?? "") && onServer(d)) || undefined;
|
|
37056
37243
|
const cutOff = (others) => {
|
|
37057
37244
|
const on = others.filter((d) => !unread(d)).map((d) => d.name).join(", ");
|
|
37058
37245
|
const maybe = others.filter(unread).map((d) => d.name).join(", ");
|
|
@@ -37073,8 +37260,13 @@ async function connect2(url, opts, deps) {
|
|
|
37073
37260
|
const prepOpts = { resetPassword: opts.resetPassword, ...storedPassword ? { storedPassword, others } : {} };
|
|
37074
37261
|
if (!opts.selfHosted) {
|
|
37075
37262
|
const probe = await deps.prepare(url, provider, { ...prepOpts, check: true });
|
|
37076
|
-
if ("next" in probe)
|
|
37077
|
-
|
|
37263
|
+
if ("next" in probe) {
|
|
37264
|
+
const { resettable, ...refusal } = probe;
|
|
37265
|
+
if (resettable && !opts.resetPassword && !opts.agent && await deps.confirm("A monitoring role from an earlier connection exists, and its password isn't stored. Reset it now? Anything else using this role will need the new password. [y/N] ")) {
|
|
37266
|
+
return connect2(url, { ...opts, resetPassword: true }, deps);
|
|
37267
|
+
}
|
|
37268
|
+
return { status: "action_required", provider, name, ...refusal };
|
|
37269
|
+
}
|
|
37078
37270
|
const b = await billingFor2(await deps.quote(opts.coupon, clusterOf2(url)), name, opts, deps, (price) => progress("billing", `Billing: ${price}`));
|
|
37079
37271
|
if (b.stop)
|
|
37080
37272
|
return { status: "action_required", provider, name, ...b.stop };
|
|
@@ -37083,10 +37275,10 @@ async function connect2(url, opts, deps) {
|
|
|
37083
37275
|
if (opts.resetPassword) {
|
|
37084
37276
|
let lock;
|
|
37085
37277
|
try {
|
|
37086
|
-
lock = await deps.resetLock(
|
|
37278
|
+
lock = await deps.resetLock(cluster);
|
|
37087
37279
|
} catch (err) {
|
|
37088
37280
|
if (err instanceof HttpStatusError && err.status === 409) {
|
|
37089
|
-
return { status: "action_required", provider, name, ...billing, next: `Another pgai connect --reset-password for ${
|
|
37281
|
+
return { status: "action_required", provider, name, ...billing, next: `Another pgai connect --reset-password for ${cluster} is running: wait for it to finish, then re-run (a run that stopped frees the server 15 minutes after it started)` };
|
|
37090
37282
|
}
|
|
37091
37283
|
if (err instanceof HttpStatusError && err.status === 404) {
|
|
37092
37284
|
return { status: "action_required", provider, name, ...billing, next: `This platform cannot lock the server for --reset-password yet: set PGAI_MON_PASSWORD to ${DEFAULT_MONITORING_USER}'s password instead` };
|
|
@@ -37103,7 +37295,7 @@ async function connect2(url, opts, deps) {
|
|
|
37103
37295
|
try {
|
|
37104
37296
|
if (lockId) {
|
|
37105
37297
|
const now = await deps.list();
|
|
37106
|
-
const same = now
|
|
37298
|
+
const same = findDatabase(now);
|
|
37107
37299
|
if (same)
|
|
37108
37300
|
return { status: "action_required", provider, name, id: same.id, ...billing, next: `${name} is already connected, and its monitoring uses the current password: pgai disconnect ${name} --yes first` };
|
|
37109
37301
|
const others = othersOnServer(now);
|
|
@@ -37112,8 +37304,10 @@ async function connect2(url, opts, deps) {
|
|
|
37112
37304
|
}
|
|
37113
37305
|
progress("preparing", `Preparing ${maskConnectionString(url)}`);
|
|
37114
37306
|
prepared = await deps.prepare(url, provider, prepOpts);
|
|
37115
|
-
if ("next" in prepared)
|
|
37116
|
-
|
|
37307
|
+
if ("next" in prepared) {
|
|
37308
|
+
const { resettable, ...refusal } = prepared;
|
|
37309
|
+
return { status: "action_required", provider, name, ...billing, ...refusal };
|
|
37310
|
+
}
|
|
37117
37311
|
if (!("monitoringUrl" in prepared))
|
|
37118
37312
|
throw new Error("prepare returned no monitoring URL");
|
|
37119
37313
|
if (prepared.note)
|
|
@@ -37174,8 +37368,9 @@ async function connect2(url, opts, deps) {
|
|
|
37174
37368
|
}
|
|
37175
37369
|
let shown;
|
|
37176
37370
|
for (const deadline = deps.now() + opts.waitMs;; ) {
|
|
37177
|
-
if (
|
|
37178
|
-
|
|
37371
|
+
if (row.billing_error && disconnecting2(row.status)) {
|
|
37372
|
+
if (undoRole)
|
|
37373
|
+
await undoRole();
|
|
37179
37374
|
return {
|
|
37180
37375
|
status: "action_required",
|
|
37181
37376
|
provider,
|
|
@@ -37183,7 +37378,7 @@ async function connect2(url, opts, deps) {
|
|
|
37183
37378
|
id: row.id,
|
|
37184
37379
|
...billing,
|
|
37185
37380
|
...checkup ? { checkup } : {},
|
|
37186
|
-
next: `The first charge failed (${row.billing_error}): the box
|
|
37381
|
+
next: `The first charge failed (${row.billing_error}): the box is being removed and nothing is billed. Update the payment method${billingUrl ? ` at ${billingUrl}` : ""}, then re-run`
|
|
37187
37382
|
};
|
|
37188
37383
|
}
|
|
37189
37384
|
const result = connectStatus2(row, provider, fresh);
|
|
@@ -37211,9 +37406,15 @@ async function connect2(url, opts, deps) {
|
|
|
37211
37406
|
}
|
|
37212
37407
|
}
|
|
37213
37408
|
async function openConnection2(url, opts) {
|
|
37409
|
+
const config = checkUrlParams2(url, opts.agent);
|
|
37214
37410
|
const conn = resolveAdminConnection({ conn: url });
|
|
37411
|
+
if (config) {
|
|
37412
|
+
const { connectionString, ...explicit } = conn.clientConfig;
|
|
37413
|
+
conn.clientConfig = { ...config, ...explicit };
|
|
37414
|
+
}
|
|
37215
37415
|
const fallback = !opts.agent && !!conn.sslFallbackEnabled;
|
|
37216
37416
|
const { client, usedSsl } = await connectWithSslFallback(opts.Client ?? Client2, { ...conn, sslFallbackEnabled: fallback });
|
|
37417
|
+
await client.query("set search_path = pg_catalog, pg_temp");
|
|
37217
37418
|
return { client, usedSsl, refusedTls: fallback && !usedSsl };
|
|
37218
37419
|
}
|
|
37219
37420
|
async function dropMonitoringRole2(client) {
|
|
@@ -37314,7 +37515,7 @@ async function prepareDatabase2(url, provider, opts = {}) {
|
|
|
37314
37515
|
const reset = !!opts.resetPassword && me.mon_exists;
|
|
37315
37516
|
if (me.mon_exists && !reset) {
|
|
37316
37517
|
if (!process.env.PGAI_MON_PASSWORD?.trim())
|
|
37317
|
-
return { next: `${exists}. ${setPassword}
|
|
37518
|
+
return { next: `${exists}. ${setPassword}`, resettable: true };
|
|
37318
37519
|
let accepted;
|
|
37319
37520
|
try {
|
|
37320
37521
|
accepted = await logsIn(loginUrlFor2(url, me.db, password));
|
|
@@ -37443,6 +37644,7 @@ function disconnectBilling(reply) {
|
|
|
37443
37644
|
function platformDeps2(p) {
|
|
37444
37645
|
const rpc = (fn, body = {}) => callRpc({ apiKey: p.apiKey, apiBaseUrl: p.apiBaseUrl, fn, body, operation: fn.replace(/_/g, " "), debug: p.debug, orgScope: p.orgScope });
|
|
37445
37646
|
return {
|
|
37647
|
+
verifyTls: verifyCollectorTls,
|
|
37446
37648
|
list: () => rpc("cloud_monitoring_list"),
|
|
37447
37649
|
create: (body) => rpc("cloud_monitoring_connect", body),
|
|
37448
37650
|
disconnect: (id) => rpc("cloud_monitoring_disconnect", { instance_id: id }),
|
|
@@ -41684,12 +41886,13 @@ async function generateD0042(client, nodeName) {
|
|
|
41684
41886
|
let pgssError = null;
|
|
41685
41887
|
const pgssSampleQueries = [];
|
|
41686
41888
|
try {
|
|
41687
|
-
const extCheck = await client.query("select
|
|
41889
|
+
const extCheck = await client.query("select n.nspname as schema from pg_catalog.pg_extension e join pg_catalog.pg_namespace n on n.oid = e.extnamespace where e.extname = 'pg_stat_statements'");
|
|
41688
41890
|
if (extCheck.rows.length > 0) {
|
|
41689
41891
|
pgssAvailable = true;
|
|
41892
|
+
const pgssView = quoteIdent4(extCheck.rows[0].schema, "pg_stat_statements");
|
|
41690
41893
|
const statsResult = await client.query(`
|
|
41691
41894
|
select count(*) as cnt, coalesce(sum(calls), 0) as total_calls
|
|
41692
|
-
from
|
|
41895
|
+
from ${pgssView}
|
|
41693
41896
|
`);
|
|
41694
41897
|
pgssMetricsCount = parseInt(statsResult.rows[0]?.cnt || "0", 10);
|
|
41695
41898
|
pgssTotalCalls = parseInt(statsResult.rows[0]?.total_calls || "0", 10);
|
|
@@ -41699,7 +41902,7 @@ async function generateD0042(client, nodeName) {
|
|
|
41699
41902
|
coalesce(usename, 'unknown') as "user",
|
|
41700
41903
|
coalesce(datname, 'unknown') as database,
|
|
41701
41904
|
calls
|
|
41702
|
-
from
|
|
41905
|
+
from ${pgssView} s
|
|
41703
41906
|
left join pg_database d on s.dbid = d.oid
|
|
41704
41907
|
left join pg_user u on s.userid = u.usesysid
|
|
41705
41908
|
order by calls desc
|
|
@@ -41727,16 +41930,17 @@ async function generateD0042(client, nodeName) {
|
|
|
41727
41930
|
let kcacheError = null;
|
|
41728
41931
|
const kcacheSampleQueries = [];
|
|
41729
41932
|
try {
|
|
41730
|
-
const extCheck = await client.query("select
|
|
41933
|
+
const extCheck = await client.query("select n.nspname as schema from pg_catalog.pg_extension e join pg_catalog.pg_namespace n on n.oid = e.extnamespace where e.extname = 'pg_stat_kcache'");
|
|
41731
41934
|
if (extCheck.rows.length > 0) {
|
|
41732
41935
|
kcacheAvailable = true;
|
|
41936
|
+
const kcacheView = quoteIdent4(extCheck.rows[0].schema, "pg_stat_kcache");
|
|
41733
41937
|
const statsResult = await client.query(`
|
|
41734
41938
|
select
|
|
41735
41939
|
count(*) as cnt,
|
|
41736
41940
|
coalesce(sum(exec_user_time + exec_system_time), 0) as total_exec_time,
|
|
41737
41941
|
coalesce(sum(exec_user_time), 0) as total_user_time,
|
|
41738
41942
|
coalesce(sum(exec_system_time), 0) as total_system_time
|
|
41739
|
-
from
|
|
41943
|
+
from ${kcacheView}
|
|
41740
41944
|
`);
|
|
41741
41945
|
kcacheMetricsCount = parseInt(statsResult.rows[0]?.cnt || "0", 10);
|
|
41742
41946
|
kcacheTotalExecTime = parseFloat(statsResult.rows[0]?.total_exec_time || "0");
|
|
@@ -41747,7 +41951,7 @@ async function generateD0042(client, nodeName) {
|
|
|
41747
41951
|
queryid::text as queryid,
|
|
41748
41952
|
coalesce(usename, 'unknown') as "user",
|
|
41749
41953
|
(exec_user_time + exec_system_time) as exec_total_time
|
|
41750
|
-
from
|
|
41954
|
+
from ${kcacheView} k
|
|
41751
41955
|
left join pg_user u on k.userid = u.usesysid
|
|
41752
41956
|
order by (exec_user_time + exec_system_time) desc
|
|
41753
41957
|
limit 5
|
|
@@ -43855,14 +44059,14 @@ function writeInstancesFile(file, instances) {
|
|
|
43855
44059
|
fs10.closeSync(fd);
|
|
43856
44060
|
}
|
|
43857
44061
|
}
|
|
43858
|
-
function
|
|
44062
|
+
function extractSslmode2(connStr) {
|
|
43859
44063
|
try {
|
|
43860
44064
|
return (new URL(connStr).searchParams.get("sslmode") || "").toLowerCase();
|
|
43861
44065
|
} catch {
|
|
43862
44066
|
return "";
|
|
43863
44067
|
}
|
|
43864
44068
|
}
|
|
43865
|
-
function
|
|
44069
|
+
function sslOptionFromSslmode2(sslmode) {
|
|
43866
44070
|
switch (sslmode) {
|
|
43867
44071
|
case "disable":
|
|
43868
44072
|
return false;
|
|
@@ -43886,7 +44090,7 @@ function isLaxSslmode(sslmode) {
|
|
|
43886
44090
|
return LAX_SSLMODES2.has(sslmode);
|
|
43887
44091
|
}
|
|
43888
44092
|
function warnIfLaxSslmode(connStr) {
|
|
43889
|
-
const sslmode =
|
|
44093
|
+
const sslmode = extractSslmode2(connStr);
|
|
43890
44094
|
if (!isLaxSslmode(sslmode))
|
|
43891
44095
|
return;
|
|
43892
44096
|
const shown = sslmode || "(unset)";
|
|
@@ -43908,23 +44112,23 @@ function warnIfTransactionPoolerPort(connStr) {
|
|
|
43908
44112
|
return;
|
|
43909
44113
|
console.error(`⚠ port ${SUPABASE_POOLER_TRANSACTION_PORT2} is the pooler's TRANSACTION mode: ` + `pooled backends share prepared statements, so collection fails with ` + `42P05 (prepared statement already exists) and metrics stop silently. ` + `Use port ${SUPABASE_POOLER_SESSION_PORT2} on the same host for session mode.`);
|
|
43910
44114
|
}
|
|
43911
|
-
function
|
|
43912
|
-
const sslmode =
|
|
44115
|
+
function buildClientConfig2(connStr, extra = {}) {
|
|
44116
|
+
const sslmode = extractSslmode2(connStr);
|
|
43913
44117
|
const channelBinding = splitChannelBinding2(connStr);
|
|
43914
44118
|
const enableChannelBinding = requireChannelBinding2(channelBinding.value, sslmode === "disable");
|
|
43915
|
-
const parsed = parse(
|
|
44119
|
+
const parsed = parse(withoutSslmode2(channelBinding.uri));
|
|
43916
44120
|
return {
|
|
43917
44121
|
host: parsed.host || undefined,
|
|
43918
44122
|
port: parsed.port ? Number(parsed.port) : undefined,
|
|
43919
44123
|
user: parsed.user,
|
|
43920
44124
|
password: parsed.password,
|
|
43921
|
-
database:
|
|
43922
|
-
ssl:
|
|
44125
|
+
database: databaseName4(withoutSslmode2(channelBinding.uri), parsed.database),
|
|
44126
|
+
ssl: sslOptionFromSslmode2(sslmode),
|
|
43923
44127
|
...enableChannelBinding ? { enableChannelBinding: true } : {},
|
|
43924
44128
|
...extra
|
|
43925
44129
|
};
|
|
43926
44130
|
}
|
|
43927
|
-
function
|
|
44131
|
+
function databaseName4(connStr, parsed) {
|
|
43928
44132
|
if (!parsed)
|
|
43929
44133
|
return;
|
|
43930
44134
|
try {
|
|
@@ -43934,7 +44138,7 @@ function databaseName3(connStr, parsed) {
|
|
|
43934
44138
|
} catch {}
|
|
43935
44139
|
return parsed;
|
|
43936
44140
|
}
|
|
43937
|
-
function
|
|
44141
|
+
function withoutSslmode2(connStr) {
|
|
43938
44142
|
try {
|
|
43939
44143
|
const u = new URL(connStr);
|
|
43940
44144
|
u.searchParams.delete("sslmode");
|
|
@@ -43956,6 +44160,47 @@ function splitChannelBinding2(uri) {
|
|
|
43956
44160
|
const params = uri.slice(start + 1, end).split("&").filter((param) => !new URLSearchParams(param).has("channel_binding"));
|
|
43957
44161
|
return { uri: uri.slice(0, start) + (params.length ? `?${params.join("&")}` : "") + uri.slice(end), value };
|
|
43958
44162
|
}
|
|
44163
|
+
async function verifyCollectorTls2(url) {
|
|
44164
|
+
const u = new URL(url);
|
|
44165
|
+
const ca = u.searchParams.get("sslrootcert");
|
|
44166
|
+
for (const key of ["sslrootcert", "sslcert", "sslkey", "uselibpqcompat"])
|
|
44167
|
+
u.searchParams.delete(key);
|
|
44168
|
+
const client = new Client2({
|
|
44169
|
+
...buildClientConfig2(splitChannelBinding2(u.toString()).uri, { connectionTimeoutMillis: 1e4 }),
|
|
44170
|
+
ssl: { rejectUnauthorized: true, servername: u.hostname, ...ca ? { ca: fs10.readFileSync(ca, "utf8") } : {} }
|
|
44171
|
+
});
|
|
44172
|
+
try {
|
|
44173
|
+
await client.connect();
|
|
44174
|
+
} catch (err) {
|
|
44175
|
+
if (!client.connection.stream.authorized)
|
|
44176
|
+
throw err;
|
|
44177
|
+
} finally {
|
|
44178
|
+
await client.end().catch(() => {});
|
|
44179
|
+
}
|
|
44180
|
+
}
|
|
44181
|
+
async function collectorConnection2(url, verifyTls = verifyCollectorTls2) {
|
|
44182
|
+
const binding = splitChannelBinding2(url);
|
|
44183
|
+
const sslmode = extractSslmode2(url);
|
|
44184
|
+
if (binding.value === "require") {
|
|
44185
|
+
if (sslmode === "disable" || sslmode === "allow") {
|
|
44186
|
+
throw new Error(`channel_binding=require needs TLS, but sslmode=${sslmode} is set`);
|
|
44187
|
+
}
|
|
44188
|
+
let collectorUrl = binding.uri;
|
|
44189
|
+
if (sslmode !== "verify-full") {
|
|
44190
|
+
const u = new URL(collectorUrl);
|
|
44191
|
+
u.searchParams.set("sslmode", "verify-full");
|
|
44192
|
+
collectorUrl = u.toString();
|
|
44193
|
+
try {
|
|
44194
|
+
await verifyTls(collectorUrl);
|
|
44195
|
+
} catch (err) {
|
|
44196
|
+
const why = err instanceof Error && err.message ? ` (${err.message})` : "";
|
|
44197
|
+
throw new Error(`Could not verify the TLS certificate for channel_binding=require${why}. Provide sslrootcert=<CA file> with sslmode=verify-full, or explicitly accept the downgrade by removing channel_binding=require from the URL`);
|
|
44198
|
+
}
|
|
44199
|
+
}
|
|
44200
|
+
return { url: collectorUrl, note: "the collector can't do channel binding; it connects with TLS and full certificate verification" + (sslmode !== "verify-full" ? "; upgraded to sslmode=verify-full" : "") };
|
|
44201
|
+
}
|
|
44202
|
+
return { url: binding.uri, ...binding.value !== null ? { note: "removed channel_binding from the connection string; the collector does not support it (TLS is kept)" } : {} };
|
|
44203
|
+
}
|
|
43959
44204
|
function requireChannelBinding2(value, sslDisabled) {
|
|
43960
44205
|
if (value !== "require")
|
|
43961
44206
|
return false;
|
|
@@ -46601,7 +46846,7 @@ async function runConnect(url, opts) {
|
|
|
46601
46846
|
const json = jsonOutput(opts.json);
|
|
46602
46847
|
const name = (() => {
|
|
46603
46848
|
try {
|
|
46604
|
-
return
|
|
46849
|
+
return databaseName3(url);
|
|
46605
46850
|
} catch {
|
|
46606
46851
|
return "";
|
|
46607
46852
|
}
|
|
@@ -46721,7 +46966,7 @@ withOrgOptions(program2.command("disconnect <name>")).description("stop monitori
|
|
|
46721
46966
|
const [row] = (await cloudDatabases(opts, name)).filter((d) => !disconnecting2(d.status));
|
|
46722
46967
|
if (!row)
|
|
46723
46968
|
throw new Error(`No database named ${name}. See: pgai databases`);
|
|
46724
|
-
if (!opts.yes && !(
|
|
46969
|
+
if (!opts.yes && !(interactive(opts.json) && /^y/i.test(await question(`Disconnect ${row.name} and delete its monitoring box? (y/N): `)))) {
|
|
46725
46970
|
return emitConnect({ status: "action_required", provider: row.provider, name: row.name, id: row.id, next: `pgai disconnect ${row.name} --yes` }, opts.json);
|
|
46726
46971
|
}
|
|
46727
46972
|
const billing = disconnectBilling(await cloudApi(opts.debug).disconnect(row.id));
|
|
@@ -46889,7 +47134,7 @@ Use demo mode without API key: postgres-ai mon local-install --demo`);
|
|
|
46889
47134
|
try {
|
|
46890
47135
|
warnIfLaxSslmode(connStr);
|
|
46891
47136
|
warnIfTransactionPoolerPort(connStr);
|
|
46892
|
-
testClient = new Client(
|
|
47137
|
+
testClient = new Client(buildClientConfig2(connStr, { connectionTimeoutMillis: 1e4 }));
|
|
46893
47138
|
await testClient.connect();
|
|
46894
47139
|
const result = await testClient.query("select version();");
|
|
46895
47140
|
console.log("\u2713 Connection successful");
|
|
@@ -46933,7 +47178,7 @@ You can provide either:`);
|
|
|
46933
47178
|
try {
|
|
46934
47179
|
warnIfLaxSslmode(connStr);
|
|
46935
47180
|
warnIfTransactionPoolerPort(connStr);
|
|
46936
|
-
testClient = new Client(
|
|
47181
|
+
testClient = new Client(buildClientConfig2(connStr, { connectionTimeoutMillis: 1e4 }));
|
|
46937
47182
|
await testClient.connect();
|
|
46938
47183
|
const result = await testClient.query("select version();");
|
|
46939
47184
|
console.log("\u2713 Connection successful");
|
|
@@ -47776,22 +48021,30 @@ async function syncVmalert(projectDir, apply, addOnly = false) {
|
|
|
47776
48021
|
}
|
|
47777
48022
|
var looksLikeConnStr = (value) => /^postgres(ql)?:\/\//i.test(value);
|
|
47778
48023
|
var isPlainTargetName = (value) => /^[A-Za-z0-9._=-]+$/.test(value) && !/(password|pwd)\s*=/i.test(value);
|
|
47779
|
-
async function addTarget(file, projectDir, connStr, name, env, { apply = true } = {}) {
|
|
48024
|
+
async function addTarget(file, projectDir, connStr, name, env, { apply = true, verifyTls } = {}) {
|
|
47780
48025
|
if (!connStr) {
|
|
47781
48026
|
console.error("Connection string required: postgresql://user:pass@host:port/db");
|
|
47782
48027
|
process.exitCode = 1;
|
|
47783
48028
|
return false;
|
|
47784
48029
|
}
|
|
47785
48030
|
const channelBinding = splitChannelBinding2(connStr);
|
|
47786
|
-
|
|
47787
|
-
const defaultName = defaultTargetName(connStr);
|
|
48031
|
+
const defaultName = defaultTargetName(channelBinding.uri);
|
|
47788
48032
|
if ("error" in defaultName) {
|
|
47789
48033
|
console.error(defaultName.error);
|
|
47790
48034
|
process.exitCode = 1;
|
|
47791
48035
|
return false;
|
|
47792
48036
|
}
|
|
47793
|
-
|
|
47794
|
-
|
|
48037
|
+
try {
|
|
48038
|
+
const collector = await collectorConnection2(connStr, verifyTls);
|
|
48039
|
+
connStr = collector.url;
|
|
48040
|
+
if (collector.note)
|
|
48041
|
+
console.error(`${channelBinding.value === "require" ? "Warning" : "Note"}: ${collector.note}`);
|
|
48042
|
+
if (channelBinding.value === "require" && caNote2(connStr))
|
|
48043
|
+
console.error(`Note: ${caNote2(connStr)}`);
|
|
48044
|
+
} catch (err) {
|
|
48045
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
48046
|
+
process.exitCode = 1;
|
|
48047
|
+
return false;
|
|
47795
48048
|
}
|
|
47796
48049
|
const instanceName = name && name.trim() ? name.trim() : defaultName.name;
|
|
47797
48050
|
if (detectProvider(connStr) !== "clickhouse" && extractProjectRefFromUrl(connStr) && supabaseHostMetricsOn(projectDir, env) && !SUPABASE_TARGET_NAME_RE.test(instanceName)) {
|
|
@@ -47991,7 +48244,7 @@ targets.command("test <name>").description("test monitoring target database conn
|
|
|
47991
48244
|
console.log(`Testing connection to monitoring target '${name}'...`);
|
|
47992
48245
|
warnIfLaxSslmode(instance.conn_str);
|
|
47993
48246
|
warnIfTransactionPoolerPort(instance.conn_str);
|
|
47994
|
-
const client = new Client(
|
|
48247
|
+
const client = new Client(buildClientConfig2(instance.conn_str, { connectionTimeoutMillis: 1e4 }));
|
|
47995
48248
|
try {
|
|
47996
48249
|
try {
|
|
47997
48250
|
await client.connect();
|
|
@@ -3,6 +3,6 @@
|
|
|
3
3
|
-- Enable pg_stat_statements for query performance monitoring
|
|
4
4
|
-- Note: Uses IF NOT EXISTS because extension may already be installed.
|
|
5
5
|
-- We do NOT drop this extension in unprepare-db since it may have been pre-existing.
|
|
6
|
-
create extension if not exists pg_stat_statements;
|
|
6
|
+
create extension if not exists pg_stat_statements with schema public;
|
|
7
7
|
|
|
8
8
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
-- Optional permissions for RDS Postgres / Aurora (best effort)
|
|
2
2
|
|
|
3
|
-
create
|
|
3
|
+
create schema if not exists rds_tools;
|
|
4
|
+
create extension if not exists rds_tools with schema rds_tools;
|
|
4
5
|
grant execute on function rds_tools.pg_ls_multixactdir() to {{ROLE_IDENT}};
|
|
5
6
|
|
|
6
7
|
|
|
@@ -3,6 +3,6 @@
|
|
|
3
3
|
-- Enable pg_stat_statements for query performance monitoring
|
|
4
4
|
-- Note: Uses IF NOT EXISTS because extension may already be installed.
|
|
5
5
|
-- We do NOT drop this extension in unprepare-db since it may have been pre-existing.
|
|
6
|
-
create extension if not exists pg_stat_statements;
|
|
6
|
+
create extension if not exists pg_stat_statements with schema public;
|
|
7
7
|
|
|
8
8
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
-- Optional permissions for RDS Postgres / Aurora (best effort)
|
|
2
2
|
|
|
3
|
-
create
|
|
3
|
+
create schema if not exists rds_tools;
|
|
4
|
+
create extension if not exists rds_tools with schema rds_tools;
|
|
4
5
|
grant execute on function rds_tools.pg_ls_multixactdir() to {{ROLE_IDENT}};
|
|
5
6
|
|
|
6
7
|
|
package/package.json
CHANGED
package/sql/02.extensions.sql
CHANGED
|
@@ -3,6 +3,6 @@
|
|
|
3
3
|
-- Enable pg_stat_statements for query performance monitoring
|
|
4
4
|
-- Note: Uses IF NOT EXISTS because extension may already be installed.
|
|
5
5
|
-- We do NOT drop this extension in unprepare-db since it may have been pre-existing.
|
|
6
|
-
create extension if not exists pg_stat_statements;
|
|
6
|
+
create extension if not exists pg_stat_statements with schema public;
|
|
7
7
|
|
|
8
8
|
|
package/sql/04.optional_rds.sql
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
-- Optional permissions for RDS Postgres / Aurora (best effort)
|
|
2
2
|
|
|
3
|
-
create
|
|
3
|
+
create schema if not exists rds_tools;
|
|
4
|
+
create extension if not exists rds_tools with schema rds_tools;
|
|
4
5
|
grant execute on function rds_tools.pg_ls_multixactdir() to {{ROLE_IDENT}};
|
|
5
6
|
|
|
6
7
|
|