postgresai 0.17.0-dev.5 → 0.17.0-dev.6
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 +51 -10
- package/dist/bin/postgres-ai.js +508 -293
- package/dist/sql/06.helpers.sql +0 -10
- package/dist/sql/sql/06.helpers.sql +0 -10
- package/package.json +1 -1
- package/schemas/A002.schema.json +0 -2
- package/schemas/A003.schema.json +0 -2
- package/schemas/A004.schema.json +0 -2
- package/schemas/A007.schema.json +0 -2
- package/schemas/A013.schema.json +0 -2
- package/schemas/D001.schema.json +0 -2
- package/schemas/D004.schema.json +0 -2
- package/schemas/F001.schema.json +0 -2
- package/schemas/F002.schema.json +1 -1
- package/schemas/F003.schema.json +0 -2
- package/schemas/F004.schema.json +0 -2
- package/schemas/F005.schema.json +0 -2
- package/schemas/F009.schema.json +0 -2
- package/schemas/G001.schema.json +0 -2
- package/schemas/G003.schema.json +0 -2
- package/schemas/H001.schema.json +0 -2
- package/schemas/H002.schema.json +0 -2
- package/schemas/H004.schema.json +0 -2
- package/schemas/I001.schema.json +0 -2
- package/schemas/K001.schema.json +0 -2
- package/schemas/K003.schema.json +0 -2
- package/schemas/K004.schema.json +0 -2
- package/schemas/K005.schema.json +0 -2
- package/schemas/K006.schema.json +0 -2
- package/schemas/K007.schema.json +0 -2
- package/schemas/K008.schema.json +0 -2
- package/schemas/M001.schema.json +0 -2
- package/schemas/M002.schema.json +0 -2
- package/schemas/M003.schema.json +0 -2
- package/schemas/N001.schema.json +0 -2
- package/sql/06.helpers.sql +0 -10
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.6",
|
|
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.6";
|
|
16897
16897
|
var package_default2 = {
|
|
16898
16898
|
name: "postgresai",
|
|
16899
16899
|
version,
|
|
@@ -21642,7 +21642,7 @@ function summarizeG003(nodeData) {
|
|
|
21642
21642
|
|
|
21643
21643
|
// lib/checkup.ts
|
|
21644
21644
|
var __dirname = "/builds/postgres-ai/postgresai/cli/lib";
|
|
21645
|
-
var CONTRACT_VERSION = "1.
|
|
21645
|
+
var CONTRACT_VERSION = "1.0.0";
|
|
21646
21646
|
var SECONDS_PER_DAY = 86400;
|
|
21647
21647
|
var SECONDS_PER_HOUR = 3600;
|
|
21648
21648
|
var SECONDS_PER_MINUTE = 60;
|
|
@@ -23975,6 +23975,38 @@ function withCheckSummary(report) {
|
|
|
23975
23975
|
return report;
|
|
23976
23976
|
}
|
|
23977
23977
|
|
|
23978
|
+
// lib/checkup-upload.ts
|
|
23979
|
+
function responseId(fn, response, ...keys) {
|
|
23980
|
+
const answer = response ?? {};
|
|
23981
|
+
const id = Number(keys.map((key) => answer[key]).find((value) => value != null));
|
|
23982
|
+
if (!Number.isFinite(id) || id <= 0) {
|
|
23983
|
+
throw new Error(typeof answer.message === "string" ? answer.message : `Unexpected ${fn} response: ${JSON.stringify(response)}`);
|
|
23984
|
+
}
|
|
23985
|
+
return id;
|
|
23986
|
+
}
|
|
23987
|
+
async function createCheckupReport(rpc, params) {
|
|
23988
|
+
const fn = "checkup_report_create";
|
|
23989
|
+
const response = await rpc(fn, {
|
|
23990
|
+
access_token: params.apiKey,
|
|
23991
|
+
project: params.project,
|
|
23992
|
+
...params.status ? { status: params.status } : {}
|
|
23993
|
+
});
|
|
23994
|
+
return { reportId: responseId(fn, response, "report_id") };
|
|
23995
|
+
}
|
|
23996
|
+
async function uploadCheckupReportJson(rpc, params) {
|
|
23997
|
+
const fn = "checkup_report_file_post";
|
|
23998
|
+
const response = await rpc(fn, {
|
|
23999
|
+
access_token: params.apiKey,
|
|
24000
|
+
checkup_report_id: params.reportId,
|
|
24001
|
+
filename: params.filename,
|
|
24002
|
+
check_id: params.checkId,
|
|
24003
|
+
data: params.jsonText,
|
|
24004
|
+
type: "json",
|
|
24005
|
+
generate_issue: true
|
|
24006
|
+
});
|
|
24007
|
+
return { reportChunkId: responseId(fn, response, "report_chunck_id", "report_chunk_id") };
|
|
24008
|
+
}
|
|
24009
|
+
|
|
23978
24010
|
// lib/host-metrics.ts
|
|
23979
24011
|
import { createHash } from "node:crypto";
|
|
23980
24012
|
import { chmodSync as chmodSync3, lstatSync, mkdirSync as mkdirSync4, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "node:fs";
|
|
@@ -27936,10 +27968,12 @@ function isNumericProjectRef(ref) {
|
|
|
27936
27968
|
// lib/connect.ts
|
|
27937
27969
|
var money = (cents, currency) => currency.toLowerCase() === "usd" ? `$${(cents / 100).toFixed(2)}` : `${(cents / 100).toFixed(2)} ${currency.toUpperCase()}`;
|
|
27938
27970
|
function priceText(q) {
|
|
27971
|
+
if (!q.billed && q.same_cluster)
|
|
27972
|
+
return `included: same database cluster as ${q.same_cluster}, no extra charge`;
|
|
27939
27973
|
if (!q.billed)
|
|
27940
27974
|
return q.free_slots.total ? `free (${q.free_slots.remaining} of ${q.free_slots.total} free slots)` : "free";
|
|
27941
27975
|
const { amount, currency, interval } = q.price;
|
|
27942
|
-
const base = `${money(amount, currency)}/${interval} per
|
|
27976
|
+
const base = `${money(amount, currency)}/${interval} per database cluster (${q.plan} plan)${q.subscription ? `, cluster ${q.quantity + 1} on the subscription` : ""}`;
|
|
27943
27977
|
if (q.promo?.valid && q.amount_after_promo !== undefined && q.amount_after_promo !== null) {
|
|
27944
27978
|
const after = `${money(q.amount_after_promo, currency)}`;
|
|
27945
27979
|
const how = `with ${q.promo.code} (${q.promo.discount_description})`;
|
|
@@ -28007,7 +28041,11 @@ function stateOf(raw) {
|
|
|
28007
28041
|
class ClickhouseKeyError extends Error {
|
|
28008
28042
|
}
|
|
28009
28043
|
var serverOf = (name) => name.split("/")[0].toLowerCase().replace(/\.(?=$|:)/, "");
|
|
28010
|
-
|
|
28044
|
+
function clusterOf(url) {
|
|
28045
|
+
const u = parseUrl(url);
|
|
28046
|
+
return u?.hostname ? `${u.hostname.toLowerCase()}:${u.port || "5432"}` : undefined;
|
|
28047
|
+
}
|
|
28048
|
+
var errorText = (err) => err instanceof AggregateError && !err.message ? err.errors.map(errorText).join("; ") : err instanceof Error ? err.message : String(err);
|
|
28011
28049
|
var urlPassword = (u) => decodeURIComponent(u.password) || u.searchParams.get("password") || "";
|
|
28012
28050
|
var URL_PARAMS_KEPT = ["sslmode", "channel_binding", "application_name"];
|
|
28013
28051
|
var URL_PARAMS_TLS = ["sslrootcert", "sslcert", "sslkey", "uselibpqcompat"];
|
|
@@ -28022,6 +28060,22 @@ function checkUrlParams(url, agent) {
|
|
|
28022
28060
|
if (moved.length)
|
|
28023
28061
|
throw new Error(`The URL's query string sets ${moved.join(" and ")}: put the host and the port in the URL itself (postgresql://user:password@host:5432/dbname), so that the server prepared is the server monitored`);
|
|
28024
28062
|
}
|
|
28063
|
+
function storedPasswordNeeds(monitoringUrl) {
|
|
28064
|
+
const query = new URL(monitoringUrl).search.replace(/^\?/, "");
|
|
28065
|
+
const parts = query ? query.split("&") : [];
|
|
28066
|
+
const keys = parts.map((p) => p.split("=")[0]);
|
|
28067
|
+
const twice = URL_PARAMS_KEPT.filter((k) => keys.indexOf(k) !== keys.lastIndexOf(k));
|
|
28068
|
+
const tls = !twice.includes("sslmode") && parts.some((p) => /^sslmode=(require|verify-ca|verify-full)$/.test(p));
|
|
28069
|
+
const needs = [
|
|
28070
|
+
...tls ? [] : [`sslmode=require (or verify-full)${twice.includes("sslmode") ? ", once" : ""}`],
|
|
28071
|
+
...twice.filter((k) => k !== "sslmode").map((k) => `${k} once`),
|
|
28072
|
+
...parts.some((p) => !p) ? ["no stray &"] : [],
|
|
28073
|
+
...keys.some((k) => k && !URL_PARAMS_KEPT.includes(k)) ? ["the parameter names without %-escapes"] : []
|
|
28074
|
+
];
|
|
28075
|
+
return needs.length ? needs.join(" and ") : undefined;
|
|
28076
|
+
}
|
|
28077
|
+
var shellWord = (s) => /^[\w./:@%+=,-]+$/.test(s) ? s : `'${s.replace(/'/g, "'\\''")}'`;
|
|
28078
|
+
var disconnectEach = (names) => names.map((n) => `pgai disconnect ${shellWord(n)} --yes`).join(" and ");
|
|
28025
28079
|
function roleUrlFor(url, db, password, kept) {
|
|
28026
28080
|
const u = new URL(url);
|
|
28027
28081
|
u.username = DEFAULT_MONITORING_USER;
|
|
@@ -28030,6 +28084,7 @@ function roleUrlFor(url, db, password, kept) {
|
|
|
28030
28084
|
for (const name of [...u.searchParams.keys()])
|
|
28031
28085
|
if (!kept.includes(name))
|
|
28032
28086
|
u.searchParams.delete(name);
|
|
28087
|
+
u.search = u.search.replace(/@/g, "%40");
|
|
28033
28088
|
return u.toString();
|
|
28034
28089
|
}
|
|
28035
28090
|
var monitoringUrlFor = (url, db, password) => roleUrlFor(url, db, password, URL_PARAMS_KEPT);
|
|
@@ -28082,19 +28137,20 @@ function checkupLines(c) {
|
|
|
28082
28137
|
];
|
|
28083
28138
|
}
|
|
28084
28139
|
var boxState = (row) => row.status === "launch_requested" && row.registered_at ? "registered" : row.status ?? "starting";
|
|
28085
|
-
async function billingFor(q, name, opts, deps) {
|
|
28140
|
+
async function billingFor(q, name, opts, deps, show) {
|
|
28086
28141
|
const billing = { price: priceText(q), requires_payment_method: q.requires_payment_method };
|
|
28087
28142
|
if (opts.coupon) {
|
|
28088
28143
|
billing.coupon = q.promo?.valid ? { code: q.promo.code, valid: true, description: q.promo.discount_description } : { code: opts.coupon, valid: false, error: q.promo?.error ?? "not valid" };
|
|
28089
28144
|
}
|
|
28090
|
-
const billingPage = { ...billing, requires_payment_method: true, next: `Add a payment method at ${deps.billingUrl(q.org_alias)}, then re-run` };
|
|
28145
|
+
const billingPage = { ...billing, requires_payment_method: true, next: `Add a payment method at ${q.billing_url ?? deps.billingUrl(q.org_alias)}, then re-run` };
|
|
28091
28146
|
const stop = (next) => ({ billing, billingPage, accepted: false, stop: { ...billing, next } });
|
|
28092
28147
|
if (billing.coupon && !billing.coupon.valid) {
|
|
28093
28148
|
return stop(`Promo code ${opts.coupon}: ${billing.coupon.error}. Nothing was changed: re-run with a valid code, or without ${opts.agent ? "coupon" : "--coupon"}`);
|
|
28094
28149
|
}
|
|
28095
28150
|
if (q.requires_payment_method)
|
|
28096
28151
|
return { billing, billingPage, accepted: false, stop: billingPage };
|
|
28097
|
-
|
|
28152
|
+
show(billing.price);
|
|
28153
|
+
if (q.billed && !opts.yes && !await deps.confirm(`Provision ${name}? (y/N): `)) {
|
|
28098
28154
|
return stop(opts.agent ? `Call connect_database again with yes: true to accept ${billing.price}` : `Re-run with --yes to accept ${billing.price}`);
|
|
28099
28155
|
}
|
|
28100
28156
|
return { billing, billingPage, accepted: q.billed };
|
|
@@ -28117,7 +28173,7 @@ async function connect(url, opts, deps) {
|
|
|
28117
28173
|
return { status: "action_required", provider, name, next: "--reset-password works with PostgresAI Cloud only (it checks what else monitors this server): connect without --self-hosted, or set PGAI_MON_PASSWORD" };
|
|
28118
28174
|
}
|
|
28119
28175
|
if (opts.selfHosted && deps.localStackRunning()) {
|
|
28120
|
-
return { status: "action_required", provider, name, next: "A monitoring stack already runs on this machine: add the database with pgai mon targets add
|
|
28176
|
+
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" };
|
|
28121
28177
|
}
|
|
28122
28178
|
const rows = opts.selfHosted ? [] : await deps.list();
|
|
28123
28179
|
let row = rows.find((d) => d.name === name && !disconnecting(d.status));
|
|
@@ -28160,51 +28216,72 @@ async function connect(url, opts, deps) {
|
|
|
28160
28216
|
await deps.unprepare(url).catch(() => false);
|
|
28161
28217
|
};
|
|
28162
28218
|
let lockId;
|
|
28163
|
-
const
|
|
28164
|
-
const
|
|
28165
|
-
|
|
28166
|
-
|
|
28167
|
-
|
|
28168
|
-
|
|
28219
|
+
const unread = (d) => /^Monitoring [0-9a-f]{6}$/.test(d.name);
|
|
28220
|
+
const othersOnServer = (list) => list.filter((d) => d.name !== name && !disconnecting(d.status) && (unread(d) || serverOf(d.name) === serverOf(name)));
|
|
28221
|
+
const storedPassword = rows.some((d) => d.monitoring_password_stored && !/delet/.test(d.status ?? "") && serverOf(d.name) === serverOf(name)) || undefined;
|
|
28222
|
+
const cutOff = (others) => {
|
|
28223
|
+
const on = others.filter((d) => !unread(d)).map((d) => d.name).join(", ");
|
|
28224
|
+
const maybe = others.filter(unread).map((d) => d.name).join(", ");
|
|
28225
|
+
const what = [on && `would cut off the monitoring of ${on} on this server`, maybe && `may cut off the monitoring of ${maybe} (the platform could not read its URL, so its server is not known)`].filter(Boolean).join(", and ");
|
|
28226
|
+
const instead = [
|
|
28227
|
+
...storedPassword ? ["connect without --reset-password (PostgresAI keeps its password for this server, and sends it only over TLS)"] : [],
|
|
28228
|
+
"set PGAI_MON_PASSWORD to its password instead",
|
|
28229
|
+
`${disconnectEach(others.map((d) => d.name))} first`
|
|
28230
|
+
];
|
|
28231
|
+
return { status: "action_required", provider, name, next: `A new password for ${DEFAULT_MONITORING_USER} ${what}: ${instead.join(", or ")}` };
|
|
28232
|
+
};
|
|
28233
|
+
const others = othersOnServer(rows).filter((d) => !unread(d)).map((d) => d.name);
|
|
28234
|
+
if (opts.resetPassword && othersOnServer(rows).length)
|
|
28235
|
+
return cutOff(othersOnServer(rows));
|
|
28169
28236
|
if (opts.coupon !== undefined && !opts.coupon.trim()) {
|
|
28170
28237
|
return { status: "action_required", provider, name, next: `${opts.agent ? "coupon" : "--coupon"} is empty: pass a promotion code, or leave ${opts.agent ? "coupon" : "--coupon"} out` };
|
|
28171
28238
|
}
|
|
28239
|
+
const prepOpts = { resetPassword: opts.resetPassword, ...storedPassword ? { storedPassword, others } : {} };
|
|
28172
28240
|
if (!opts.selfHosted) {
|
|
28173
|
-
const
|
|
28241
|
+
const probe = await deps.prepare(url, provider, { ...prepOpts, check: true });
|
|
28242
|
+
if ("next" in probe)
|
|
28243
|
+
return { status: "action_required", provider, name, ...probe };
|
|
28244
|
+
const b = await billingFor(await deps.quote(opts.coupon, clusterOf(url)), name, opts, deps, (price) => progress("billing", `Billing: ${price}`));
|
|
28174
28245
|
if (b.stop)
|
|
28175
28246
|
return { status: "action_required", provider, name, ...b.stop };
|
|
28176
28247
|
({ billing, billingPage, accepted } = b);
|
|
28177
|
-
progress("billing", `Billing: ${billing.price}`);
|
|
28178
28248
|
}
|
|
28179
28249
|
if (opts.resetPassword) {
|
|
28250
|
+
let lock;
|
|
28180
28251
|
try {
|
|
28181
|
-
|
|
28252
|
+
lock = await deps.resetLock(serverOf(name));
|
|
28182
28253
|
} catch (err) {
|
|
28183
28254
|
if (err instanceof HttpStatusError && err.status === 409) {
|
|
28184
|
-
return { status: "action_required", provider, name, next: `Another pgai connect --reset-password for ${serverOf(name)} is running: wait for it to finish, then re-run` };
|
|
28255
|
+
return { status: "action_required", provider, name, ...billing, next: `Another pgai connect --reset-password for ${serverOf(name)} is running: wait for it to finish, then re-run (a run that stopped frees the server 15 minutes after it started)` };
|
|
28185
28256
|
}
|
|
28186
28257
|
if (err instanceof HttpStatusError && err.status === 404) {
|
|
28187
|
-
return { status: "action_required", provider, name, next: `This platform cannot lock the server for --reset-password yet: set PGAI_MON_PASSWORD to ${DEFAULT_MONITORING_USER}'s password instead` };
|
|
28258
|
+
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` };
|
|
28188
28259
|
}
|
|
28189
28260
|
throw err;
|
|
28190
28261
|
}
|
|
28262
|
+
if (typeof lock.lock_id !== "string" || !lock.lock_id)
|
|
28263
|
+
throw new Error("cloud_monitoring_reset_lock returned no lock_id: nothing was changed");
|
|
28264
|
+
lockId = lock.lock_id;
|
|
28191
28265
|
}
|
|
28192
28266
|
let prepared;
|
|
28193
28267
|
let created;
|
|
28268
|
+
let unanswered = false;
|
|
28194
28269
|
try {
|
|
28195
28270
|
if (lockId) {
|
|
28196
28271
|
const now = await deps.list();
|
|
28197
28272
|
const same = now.find((d) => d.name === name && !disconnecting(d.status));
|
|
28198
28273
|
if (same)
|
|
28199
|
-
return { status: "action_required", provider, name, id: same.id, next: `${name} is already connected, and its monitoring uses the current password: pgai disconnect ${name} --yes first` };
|
|
28274
|
+
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` };
|
|
28200
28275
|
const others = othersOnServer(now);
|
|
28201
28276
|
if (others.length)
|
|
28202
|
-
return cutOff(others);
|
|
28277
|
+
return { ...cutOff(others), ...billing };
|
|
28203
28278
|
}
|
|
28204
28279
|
progress("preparing", `Preparing ${maskConnectionString(url)}`);
|
|
28205
|
-
prepared = await deps.prepare(url, provider,
|
|
28280
|
+
prepared = await deps.prepare(url, provider, prepOpts);
|
|
28206
28281
|
if ("next" in prepared)
|
|
28207
28282
|
return { status: "action_required", provider, name, ...billing, ...prepared };
|
|
28283
|
+
if (!("monitoringUrl" in prepared))
|
|
28284
|
+
throw new Error("prepare returned no monitoring URL");
|
|
28208
28285
|
if (prepared.note)
|
|
28209
28286
|
note = `; ${prepared.note}`;
|
|
28210
28287
|
if (opts.selfHosted) {
|
|
@@ -28221,10 +28298,14 @@ async function connect(url, opts, deps) {
|
|
|
28221
28298
|
}).catch(async (err) => {
|
|
28222
28299
|
if (err instanceof HttpStatusError && err.status >= 400 && err.status < 500)
|
|
28223
28300
|
await undo(prepared);
|
|
28301
|
+
else
|
|
28302
|
+
unanswered = true;
|
|
28224
28303
|
if (err instanceof HttpStatusError && err.status === 402)
|
|
28225
28304
|
return /declined/i.test(err.message) ? "declined" : "payment";
|
|
28226
28305
|
if (err instanceof HttpStatusError && err.status === 412)
|
|
28227
28306
|
return "price";
|
|
28307
|
+
if (err instanceof HttpStatusError && err.status === 409 && "storedPassword" in prepared && prepared.storedPassword && /none is stored/.test(err.message))
|
|
28308
|
+
return "stored";
|
|
28228
28309
|
throw err;
|
|
28229
28310
|
});
|
|
28230
28311
|
if (created === "payment")
|
|
@@ -28234,18 +28315,26 @@ async function connect(url, opts, deps) {
|
|
|
28234
28315
|
}
|
|
28235
28316
|
if (created === "price")
|
|
28236
28317
|
return { status: "action_required", provider, name, ...billing, next: "The price changed since it was shown: re-run pgai connect to see it" };
|
|
28318
|
+
if (created === "stored") {
|
|
28319
|
+
const gone = `PostgresAI no longer keeps the password of ${DEFAULT_MONITORING_USER} for this server`;
|
|
28320
|
+
return { status: "action_required", provider, name, ...billing, next: opts.agent ? `${gone}: pass its URL as database_url, or run pgai connect <admin-url> --reset-password in a terminal` : `${gone}: re-run with --reset-password (anything else that logs in as ${DEFAULT_MONITORING_USER} then needs the new one), or set PGAI_MON_PASSWORD to its password` };
|
|
28321
|
+
}
|
|
28237
28322
|
if (created.status === "failed") {
|
|
28238
28323
|
await undo(prepared);
|
|
28239
28324
|
return { status: "failed", provider, name, id: created.id, next: `${created.error} Re-run pgai connect later.` };
|
|
28240
28325
|
}
|
|
28241
28326
|
} finally {
|
|
28242
|
-
if (lockId)
|
|
28327
|
+
if (lockId && !unanswered)
|
|
28243
28328
|
await deps.resetUnlock(lockId).catch(() => {});
|
|
28244
28329
|
}
|
|
28245
28330
|
row = { id: created.id, name: created.name, provider, status: created.status, dashboard_url: null, host_metrics: !!ch };
|
|
28246
|
-
|
|
28247
|
-
|
|
28331
|
+
if (prepared.storedPassword) {
|
|
28332
|
+
progress("checkup", `Express checkup skipped: it runs only as ${DEFAULT_MONITORING_USER}, whose password PostgresAI keeps for this server. The full checkup follows on the box.`);
|
|
28333
|
+
} else {
|
|
28334
|
+
checkup = await deps.checkup(withLocalTls(prepared.monitoringUrl, url), created.name).catch((err) => ({ error: errorText(err) }));
|
|
28335
|
+
progress("checkup", checkupLines(checkup).join(`
|
|
28248
28336
|
`), { checkup });
|
|
28337
|
+
}
|
|
28249
28338
|
}
|
|
28250
28339
|
let shown;
|
|
28251
28340
|
for (const deadline = deps.now() + opts.waitMs;; ) {
|
|
@@ -28265,9 +28354,11 @@ async function connect(url, opts, deps) {
|
|
|
28265
28354
|
row = (await deps.list().catch(() => [])).find((d) => d.id === row.id) ?? row;
|
|
28266
28355
|
}
|
|
28267
28356
|
}
|
|
28268
|
-
function openConnection(url, opts) {
|
|
28357
|
+
async function openConnection(url, opts) {
|
|
28269
28358
|
const conn = resolveAdminConnection({ conn: url });
|
|
28270
|
-
|
|
28359
|
+
const fallback = !opts.agent && !!conn.sslFallbackEnabled;
|
|
28360
|
+
const { client, usedSsl } = await connectWithSslFallback(opts.Client ?? Client2, { ...conn, sslFallbackEnabled: fallback });
|
|
28361
|
+
return { client, usedSsl, refusedTls: fallback && !usedSsl };
|
|
28271
28362
|
}
|
|
28272
28363
|
async function dropMonitoringRole(client) {
|
|
28273
28364
|
for (const sql of [`drop role ${DEFAULT_MONITORING_USER}`, `drop owned by ${DEFAULT_MONITORING_USER}; drop role ${DEFAULT_MONITORING_USER}`]) {
|
|
@@ -28300,7 +28391,7 @@ async function prepareDatabase(url, provider, opts = {}) {
|
|
|
28300
28391
|
throw err;
|
|
28301
28392
|
}
|
|
28302
28393
|
};
|
|
28303
|
-
const { client } = await open(url).catch((err) => {
|
|
28394
|
+
const { client, usedSsl, refusedTls } = await open(url).catch((err) => {
|
|
28304
28395
|
if (/client password must be a string/.test(String(err?.message)))
|
|
28305
28396
|
throw new Error("The URL has no password (and PGPASSWORD is not set): pgai connect postgresql://user:password@host:5432/dbname");
|
|
28306
28397
|
throw err;
|
|
@@ -28314,7 +28405,7 @@ async function prepareDatabase(url, provider, opts = {}) {
|
|
|
28314
28405
|
and has_database_privilege(current_user, current_database(), 'CREATE')
|
|
28315
28406
|
and exists (select 1 from pg_extension where extname = 'pg_stat_statements')) as admin,
|
|
28316
28407
|
exists (select 1 from pg_roles where rolname = '${DEFAULT_MONITORING_USER}') as mon_exists,
|
|
28317
|
-
current_setting('scram_iterations', true) as iterations
|
|
28408
|
+
current_setting('scram_iterations', true) as iterations, current_setting('ssl') as ssl
|
|
28318
28409
|
from pg_roles where rolname = current_user`)).rows[0];
|
|
28319
28410
|
const notes = [caNote(url)].filter((n) => !!n);
|
|
28320
28411
|
const noted = () => notes.length ? { note: notes.join("; ") } : {};
|
|
@@ -28338,9 +28429,31 @@ async function prepareDatabase(url, provider, opts = {}) {
|
|
|
28338
28429
|
}
|
|
28339
28430
|
} else if (me.admin && pgProvider !== "supabase") {
|
|
28340
28431
|
const exists = `${DEFAULT_MONITORING_USER} already exists on this server`;
|
|
28432
|
+
const setPassword = `Set PGAI_MON_PASSWORD to the password of ${DEFAULT_MONITORING_USER}, or give it a new one: pgai connect <admin-url> --reset-password (anything else that logs in as ${DEFAULT_MONITORING_USER} then needs the new password)`;
|
|
28433
|
+
const needs = storedPasswordNeeds(monitoringUrlFor(url, me.db, ""));
|
|
28434
|
+
const noTls = refusedTls || !usedSsl && me.ssl === "off";
|
|
28435
|
+
const tlsOnly = `PostgresAI sends the password it keeps for ${DEFAULT_MONITORING_USER} only over TLS`;
|
|
28436
|
+
const others = opts.others?.length ? opts.others : undefined;
|
|
28437
|
+
const disconnectOthers = others ? disconnectEach(others) : "pgai disconnect the server's other databases (pgai databases)";
|
|
28438
|
+
const reconnectOthers = `(and connect ${others ? others.join(", ") : "them"} again with it)`;
|
|
28439
|
+
const noTlsWayOut = opts.agent ? `Pass its URL as database_url, or run pgai connect in a terminal with PGAI_MON_PASSWORD set to its password. If nobody has it, in a terminal: ${disconnectOthers}, then pgai connect <admin-url> --reset-password with PGAI_MON_PASSWORD set to a new one ${reconnectOthers}` : `Set PGAI_MON_PASSWORD to its password, or turn on TLS on the server (ssl = on) and put sslmode=require in the URL. If nobody has the password: ${disconnectOthers}, then re-run with --reset-password and PGAI_MON_PASSWORD set to a new one ${reconnectOthers}`;
|
|
28440
|
+
const reuse = me.mon_exists && !opts.resetPassword && !!opts.storedPassword && !(opts.agent ? "" : process.env.PGAI_MON_PASSWORD?.trim());
|
|
28441
|
+
if (reuse && needs) {
|
|
28442
|
+
if (noTls)
|
|
28443
|
+
return { next: `${exists}, but this server takes no TLS, and ${tlsOnly}. ${noTlsWayOut}` };
|
|
28444
|
+
return { next: `${exists}. ${opts.agent ? "database_url" : "The URL"} needs ${needs}: the password PostgresAI keeps for this server is sent only over TLS, to a URL with each parameter once` };
|
|
28445
|
+
}
|
|
28446
|
+
if (reuse) {
|
|
28447
|
+
if (opts.check)
|
|
28448
|
+
return { checked: true };
|
|
28449
|
+
const { password: unused } = await resolveMonitoringPassword({ monitoringUser: DEFAULT_MONITORING_USER });
|
|
28450
|
+
const plan = await buildInitPlan({ database: me.db, monitoringPassword: unused, iterations: Number(me.iterations), includeOptionalPermissions: true, provider: pgProvider, keepExistingPassword: true });
|
|
28451
|
+
await applyInitPlan({ client, plan });
|
|
28452
|
+
return { monitoringUrl: monitoringUrlFor(url, me.db, ""), storedPassword: true, ...noted() };
|
|
28453
|
+
}
|
|
28341
28454
|
if (me.mon_exists && opts.agent)
|
|
28342
28455
|
return { next: `${exists}. Pass its URL as database_url, or run pgai connect in a terminal with PGAI_MON_PASSWORD set to its password` };
|
|
28343
|
-
const
|
|
28456
|
+
const unset = !opts.storedPassword ? setPassword : noTls ? `This server takes no TLS, and ${tlsOnly}. ${noTlsWayOut}` : !needs ? `Unset PGAI_MON_PASSWORD: PostgresAI keeps the password of ${DEFAULT_MONITORING_USER} for this server` : `Unset PGAI_MON_PASSWORD, and the URL needs ${needs}: PostgresAI keeps the password of ${DEFAULT_MONITORING_USER} for this server, and sends it only over TLS, to a URL with each parameter once`;
|
|
28344
28457
|
const { password, generated } = await resolveMonitoringPassword({ passwordEnv: opts.agent ? undefined : process.env.PGAI_MON_PASSWORD, monitoringUser: DEFAULT_MONITORING_USER });
|
|
28345
28458
|
const reset = !!opts.resetPassword && me.mon_exists;
|
|
28346
28459
|
if (me.mon_exists && !reset) {
|
|
@@ -28350,13 +28463,16 @@ async function prepareDatabase(url, provider, opts = {}) {
|
|
|
28350
28463
|
try {
|
|
28351
28464
|
accepted = await logsIn(loginUrlFor(url, me.db, password));
|
|
28352
28465
|
} catch (err) {
|
|
28353
|
-
|
|
28466
|
+
const orUnset = opts.storedPassword && !noTls ? ", or unset PGAI_MON_PASSWORD: PostgresAI keeps its password for this server" : "";
|
|
28467
|
+
return { next: `${exists}, and PGAI_MON_PASSWORD could not be checked from this host (${err instanceof Error ? err.message : String(err)}). Run pgai connect from a host that ${DEFAULT_MONITORING_USER} may connect from${orUnset}` };
|
|
28354
28468
|
}
|
|
28355
28469
|
if (!accepted)
|
|
28356
|
-
return { next: `${exists} and PGAI_MON_PASSWORD is not its password. ${
|
|
28470
|
+
return { next: `${exists} and PGAI_MON_PASSWORD is not its password. ${unset}` };
|
|
28357
28471
|
if (await acceptsAnyPassword())
|
|
28358
28472
|
notes.push(unchecked("PGAI_MON_PASSWORD"));
|
|
28359
28473
|
}
|
|
28474
|
+
if (opts.check)
|
|
28475
|
+
return { checked: true };
|
|
28360
28476
|
const monitoringUrl = monitoringUrlFor(url, me.db, password);
|
|
28361
28477
|
const loginUrl = loginUrlFor(url, me.db, password);
|
|
28362
28478
|
const plan = await buildInitPlan({ database: me.db, monitoringPassword: password, iterations: Number(me.iterations), includeOptionalPermissions: true, provider: pgProvider, keepExistingPassword: !reset });
|
|
@@ -28404,26 +28520,17 @@ async function expressCheckup(url, opts = {}) {
|
|
|
28404
28520
|
}
|
|
28405
28521
|
}
|
|
28406
28522
|
async function saveCheckupReport(rpc, accessToken, project, reports) {
|
|
28407
|
-
const
|
|
28408
|
-
const a = answer ?? {};
|
|
28409
|
-
const id = Number(keys.map((k) => a[k]).find((v) => v !== undefined));
|
|
28410
|
-
if (!(id > 0))
|
|
28411
|
-
throw new Error(typeof a.message === "string" ? a.message : `Unexpected ${fn} answer: ${JSON.stringify(answer)}`);
|
|
28412
|
-
return id;
|
|
28413
|
-
};
|
|
28414
|
-
const reportId = idOf("checkup_report_create", await rpc("checkup_report_create", { access_token: accessToken, project }), "report_id");
|
|
28523
|
+
const { reportId } = await createCheckupReport(rpc, { apiKey: accessToken, project });
|
|
28415
28524
|
const setStatus = (status) => rpc("checkup_report_status_update", { access_token: accessToken, report_id: reportId, status });
|
|
28416
28525
|
try {
|
|
28417
28526
|
for (const [checkId, report] of Object.entries(reports)) {
|
|
28418
|
-
|
|
28419
|
-
|
|
28420
|
-
|
|
28527
|
+
await uploadCheckupReportJson(rpc, {
|
|
28528
|
+
apiKey: accessToken,
|
|
28529
|
+
reportId,
|
|
28421
28530
|
filename: `${checkId}.json`,
|
|
28422
|
-
|
|
28423
|
-
|
|
28424
|
-
|
|
28425
|
-
generate_issue: true
|
|
28426
|
-
}), "report_chunck_id", "report_chunk_id");
|
|
28531
|
+
checkId,
|
|
28532
|
+
jsonText: JSON.stringify(report, null, 2)
|
|
28533
|
+
});
|
|
28427
28534
|
}
|
|
28428
28535
|
} catch (err) {
|
|
28429
28536
|
await setStatus("failed").catch(() => {});
|
|
@@ -28473,7 +28580,7 @@ function platformDeps(p) {
|
|
|
28473
28580
|
unprepare: (url) => unprepareDatabase(url, { agent: p.agent }),
|
|
28474
28581
|
clickhouseOrg: clickhouseOrgFor,
|
|
28475
28582
|
checkup: (url, project) => expressCheckup(url, { agent: p.agent, save: (reports) => saveCheckupReport(rpc, p.apiKey, project, reports) }),
|
|
28476
|
-
quote: (coupon) => rpc("cloud_monitoring_quote", coupon ? { promo_code: coupon } : {}),
|
|
28583
|
+
quote: (coupon, cluster) => rpc("cloud_monitoring_quote", { ...coupon ? { promo_code: coupon } : {}, ...cluster ? { db_server: cluster } : {} }),
|
|
28477
28584
|
billingUrl: (orgAlias) => `${p.uiBaseUrl}/${orgAlias}/billing`,
|
|
28478
28585
|
handoffUrl: async (provider) => {
|
|
28479
28586
|
const orgs = await listOrgs({ apiKey: p.apiKey, apiBaseUrl: p.apiBaseUrl });
|
|
@@ -35744,7 +35851,7 @@ async function startMcpServer(rootOpts, extra) {
|
|
|
35744
35851
|
},
|
|
35745
35852
|
{
|
|
35746
35853
|
name: "connect_database",
|
|
35747
|
-
description: "Put a Postgres database under PostgresAI Cloud monitoring (same as `pgai connect`): prepares the monitoring role (an admin URL creates it; otherwise returns the SQL), provisions the monitoring box, and returns JSON with status (connected | provisioning | disconnecting | action_required | failed), dashboard_url and next (the exact next action). A billed box is provisioned only with yes: true: without it the result is action_required with the price (and requires_payment_method), so show the price to the user first. It does not wait for the box: call it again to see the status. Safe to call again. ClickHouse Managed Postgres, RDS and Supabase are detected from the host. The URL must carry its password, and only these query parameters: sslmode, channel_binding, application_name. PGAI_MON_PASSWORD, PGPASSWORD and CLICKHOUSE_KEY_ID + CLICKHOUSE_KEY_SECRET of the server process are not used (pass clickhouse_key), and a TLS failure is not retried in plaintext (say sslmode=disable for a server without TLS).",
|
|
35854
|
+
description: "Put a Postgres database under PostgresAI Cloud monitoring (same as `pgai connect`): prepares the monitoring role (an admin URL creates it; otherwise returns the SQL), provisions the monitoring box, and returns JSON with status (connected | provisioning | disconnecting | action_required | failed), dashboard_url and next (the exact next action). A billed box is provisioned only with yes: true: without it the result is action_required with the price (and requires_payment_method), so show the price to the user first. It does not wait for the box: call it again to see the status. Safe to call again. ClickHouse Managed Postgres, RDS and Supabase are detected from the host. The URL must carry its password, and only these query parameters: sslmode, channel_binding, application_name. PGAI_MON_PASSWORD, PGPASSWORD and CLICKHOUSE_KEY_ID + CLICKHOUSE_KEY_SECRET of the server process are not used (pass clickhouse_key), and a TLS failure is not retried in plaintext (say sslmode=disable for a server without TLS). For another database on a server PostgresAI already monitors for the org, an admin URL with sslmode=require (or verify-*), each query parameter once, is enough: PostgresAI fills in the password of postgres_ai_mon it keeps, over TLS only (on a server without TLS, pass the postgres_ai_mon URL; if nobody has its password, the user runs in a terminal: pgai disconnect the server's other databases, then pgai connect <admin-url> --reset-password with PGAI_MON_PASSWORD set to a new one, then connects them again with it).",
|
|
35748
35855
|
inputSchema: {
|
|
35749
35856
|
type: "object",
|
|
35750
35857
|
properties: {
|
|
@@ -36658,10 +36765,12 @@ function renderMarkdownForTerminal(md) {
|
|
|
36658
36765
|
// lib/connect.ts
|
|
36659
36766
|
var money2 = (cents, currency) => currency.toLowerCase() === "usd" ? `$${(cents / 100).toFixed(2)}` : `${(cents / 100).toFixed(2)} ${currency.toUpperCase()}`;
|
|
36660
36767
|
function priceText2(q) {
|
|
36768
|
+
if (!q.billed && q.same_cluster)
|
|
36769
|
+
return `included: same database cluster as ${q.same_cluster}, no extra charge`;
|
|
36661
36770
|
if (!q.billed)
|
|
36662
36771
|
return q.free_slots.total ? `free (${q.free_slots.remaining} of ${q.free_slots.total} free slots)` : "free";
|
|
36663
36772
|
const { amount, currency, interval } = q.price;
|
|
36664
|
-
const base = `${money2(amount, currency)}/${interval} per
|
|
36773
|
+
const base = `${money2(amount, currency)}/${interval} per database cluster (${q.plan} plan)${q.subscription ? `, cluster ${q.quantity + 1} on the subscription` : ""}`;
|
|
36665
36774
|
if (q.promo?.valid && q.amount_after_promo !== undefined && q.amount_after_promo !== null) {
|
|
36666
36775
|
const after = `${money2(q.amount_after_promo, currency)}`;
|
|
36667
36776
|
const how = `with ${q.promo.code} (${q.promo.discount_description})`;
|
|
@@ -36729,7 +36838,11 @@ function stateOf2(raw) {
|
|
|
36729
36838
|
class ClickhouseKeyError2 extends Error {
|
|
36730
36839
|
}
|
|
36731
36840
|
var serverOf2 = (name) => name.split("/")[0].toLowerCase().replace(/\.(?=$|:)/, "");
|
|
36732
|
-
|
|
36841
|
+
function clusterOf2(url) {
|
|
36842
|
+
const u = parseUrl2(url);
|
|
36843
|
+
return u?.hostname ? `${u.hostname.toLowerCase()}:${u.port || "5432"}` : undefined;
|
|
36844
|
+
}
|
|
36845
|
+
var errorText2 = (err) => err instanceof AggregateError && !err.message ? err.errors.map(errorText2).join("; ") : err instanceof Error ? err.message : String(err);
|
|
36733
36846
|
var urlPassword2 = (u) => decodeURIComponent(u.password) || u.searchParams.get("password") || "";
|
|
36734
36847
|
var URL_PARAMS_KEPT2 = ["sslmode", "channel_binding", "application_name"];
|
|
36735
36848
|
var URL_PARAMS_TLS2 = ["sslrootcert", "sslcert", "sslkey", "uselibpqcompat"];
|
|
@@ -36744,6 +36857,22 @@ function checkUrlParams2(url, agent) {
|
|
|
36744
36857
|
if (moved.length)
|
|
36745
36858
|
throw new Error(`The URL's query string sets ${moved.join(" and ")}: put the host and the port in the URL itself (postgresql://user:password@host:5432/dbname), so that the server prepared is the server monitored`);
|
|
36746
36859
|
}
|
|
36860
|
+
function storedPasswordNeeds2(monitoringUrl) {
|
|
36861
|
+
const query = new URL(monitoringUrl).search.replace(/^\?/, "");
|
|
36862
|
+
const parts = query ? query.split("&") : [];
|
|
36863
|
+
const keys = parts.map((p) => p.split("=")[0]);
|
|
36864
|
+
const twice = URL_PARAMS_KEPT2.filter((k) => keys.indexOf(k) !== keys.lastIndexOf(k));
|
|
36865
|
+
const tls = !twice.includes("sslmode") && parts.some((p) => /^sslmode=(require|verify-ca|verify-full)$/.test(p));
|
|
36866
|
+
const needs = [
|
|
36867
|
+
...tls ? [] : [`sslmode=require (or verify-full)${twice.includes("sslmode") ? ", once" : ""}`],
|
|
36868
|
+
...twice.filter((k) => k !== "sslmode").map((k) => `${k} once`),
|
|
36869
|
+
...parts.some((p) => !p) ? ["no stray &"] : [],
|
|
36870
|
+
...keys.some((k) => k && !URL_PARAMS_KEPT2.includes(k)) ? ["the parameter names without %-escapes"] : []
|
|
36871
|
+
];
|
|
36872
|
+
return needs.length ? needs.join(" and ") : undefined;
|
|
36873
|
+
}
|
|
36874
|
+
var shellWord2 = (s) => /^[\w./:@%+=,-]+$/.test(s) ? s : `'${s.replace(/'/g, "'\\''")}'`;
|
|
36875
|
+
var disconnectEach2 = (names) => names.map((n) => `pgai disconnect ${shellWord2(n)} --yes`).join(" and ");
|
|
36747
36876
|
function roleUrlFor2(url, db, password, kept) {
|
|
36748
36877
|
const u = new URL(url);
|
|
36749
36878
|
u.username = DEFAULT_MONITORING_USER;
|
|
@@ -36752,6 +36881,7 @@ function roleUrlFor2(url, db, password, kept) {
|
|
|
36752
36881
|
for (const name of [...u.searchParams.keys()])
|
|
36753
36882
|
if (!kept.includes(name))
|
|
36754
36883
|
u.searchParams.delete(name);
|
|
36884
|
+
u.search = u.search.replace(/@/g, "%40");
|
|
36755
36885
|
return u.toString();
|
|
36756
36886
|
}
|
|
36757
36887
|
var monitoringUrlFor2 = (url, db, password) => roleUrlFor2(url, db, password, URL_PARAMS_KEPT2);
|
|
@@ -36813,19 +36943,20 @@ function progressText(e) {
|
|
|
36813
36943
|
`);
|
|
36814
36944
|
}
|
|
36815
36945
|
var boxState2 = (row) => row.status === "launch_requested" && row.registered_at ? "registered" : row.status ?? "starting";
|
|
36816
|
-
async function billingFor2(q, name, opts, deps) {
|
|
36946
|
+
async function billingFor2(q, name, opts, deps, show) {
|
|
36817
36947
|
const billing = { price: priceText2(q), requires_payment_method: q.requires_payment_method };
|
|
36818
36948
|
if (opts.coupon) {
|
|
36819
36949
|
billing.coupon = q.promo?.valid ? { code: q.promo.code, valid: true, description: q.promo.discount_description } : { code: opts.coupon, valid: false, error: q.promo?.error ?? "not valid" };
|
|
36820
36950
|
}
|
|
36821
|
-
const billingPage = { ...billing, requires_payment_method: true, next: `Add a payment method at ${deps.billingUrl(q.org_alias)}, then re-run` };
|
|
36951
|
+
const billingPage = { ...billing, requires_payment_method: true, next: `Add a payment method at ${q.billing_url ?? deps.billingUrl(q.org_alias)}, then re-run` };
|
|
36822
36952
|
const stop = (next) => ({ billing, billingPage, accepted: false, stop: { ...billing, next } });
|
|
36823
36953
|
if (billing.coupon && !billing.coupon.valid) {
|
|
36824
36954
|
return stop(`Promo code ${opts.coupon}: ${billing.coupon.error}. Nothing was changed: re-run with a valid code, or without ${opts.agent ? "coupon" : "--coupon"}`);
|
|
36825
36955
|
}
|
|
36826
36956
|
if (q.requires_payment_method)
|
|
36827
36957
|
return { billing, billingPage, accepted: false, stop: billingPage };
|
|
36828
|
-
|
|
36958
|
+
show(billing.price);
|
|
36959
|
+
if (q.billed && !opts.yes && !await deps.confirm(`Provision ${name}? (y/N): `)) {
|
|
36829
36960
|
return stop(opts.agent ? `Call connect_database again with yes: true to accept ${billing.price}` : `Re-run with --yes to accept ${billing.price}`);
|
|
36830
36961
|
}
|
|
36831
36962
|
return { billing, billingPage, accepted: q.billed };
|
|
@@ -36848,7 +36979,7 @@ async function connect2(url, opts, deps) {
|
|
|
36848
36979
|
return { status: "action_required", provider, name, next: "--reset-password works with PostgresAI Cloud only (it checks what else monitors this server): connect without --self-hosted, or set PGAI_MON_PASSWORD" };
|
|
36849
36980
|
}
|
|
36850
36981
|
if (opts.selfHosted && deps.localStackRunning()) {
|
|
36851
|
-
return { status: "action_required", provider, name, next: "A monitoring stack already runs on this machine: add the database with pgai mon targets add
|
|
36982
|
+
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" };
|
|
36852
36983
|
}
|
|
36853
36984
|
const rows = opts.selfHosted ? [] : await deps.list();
|
|
36854
36985
|
let row = rows.find((d) => d.name === name && !disconnecting2(d.status));
|
|
@@ -36891,51 +37022,72 @@ async function connect2(url, opts, deps) {
|
|
|
36891
37022
|
await deps.unprepare(url).catch(() => false);
|
|
36892
37023
|
};
|
|
36893
37024
|
let lockId;
|
|
36894
|
-
const
|
|
36895
|
-
const
|
|
36896
|
-
|
|
36897
|
-
|
|
36898
|
-
|
|
36899
|
-
|
|
37025
|
+
const unread = (d) => /^Monitoring [0-9a-f]{6}$/.test(d.name);
|
|
37026
|
+
const othersOnServer = (list) => list.filter((d) => d.name !== name && !disconnecting2(d.status) && (unread(d) || serverOf2(d.name) === serverOf2(name)));
|
|
37027
|
+
const storedPassword = rows.some((d) => d.monitoring_password_stored && !/delet/.test(d.status ?? "") && serverOf2(d.name) === serverOf2(name)) || undefined;
|
|
37028
|
+
const cutOff = (others) => {
|
|
37029
|
+
const on = others.filter((d) => !unread(d)).map((d) => d.name).join(", ");
|
|
37030
|
+
const maybe = others.filter(unread).map((d) => d.name).join(", ");
|
|
37031
|
+
const what = [on && `would cut off the monitoring of ${on} on this server`, maybe && `may cut off the monitoring of ${maybe} (the platform could not read its URL, so its server is not known)`].filter(Boolean).join(", and ");
|
|
37032
|
+
const instead = [
|
|
37033
|
+
...storedPassword ? ["connect without --reset-password (PostgresAI keeps its password for this server, and sends it only over TLS)"] : [],
|
|
37034
|
+
"set PGAI_MON_PASSWORD to its password instead",
|
|
37035
|
+
`${disconnectEach2(others.map((d) => d.name))} first`
|
|
37036
|
+
];
|
|
37037
|
+
return { status: "action_required", provider, name, next: `A new password for ${DEFAULT_MONITORING_USER} ${what}: ${instead.join(", or ")}` };
|
|
37038
|
+
};
|
|
37039
|
+
const others = othersOnServer(rows).filter((d) => !unread(d)).map((d) => d.name);
|
|
37040
|
+
if (opts.resetPassword && othersOnServer(rows).length)
|
|
37041
|
+
return cutOff(othersOnServer(rows));
|
|
36900
37042
|
if (opts.coupon !== undefined && !opts.coupon.trim()) {
|
|
36901
37043
|
return { status: "action_required", provider, name, next: `${opts.agent ? "coupon" : "--coupon"} is empty: pass a promotion code, or leave ${opts.agent ? "coupon" : "--coupon"} out` };
|
|
36902
37044
|
}
|
|
37045
|
+
const prepOpts = { resetPassword: opts.resetPassword, ...storedPassword ? { storedPassword, others } : {} };
|
|
36903
37046
|
if (!opts.selfHosted) {
|
|
36904
|
-
const
|
|
37047
|
+
const probe = await deps.prepare(url, provider, { ...prepOpts, check: true });
|
|
37048
|
+
if ("next" in probe)
|
|
37049
|
+
return { status: "action_required", provider, name, ...probe };
|
|
37050
|
+
const b = await billingFor2(await deps.quote(opts.coupon, clusterOf2(url)), name, opts, deps, (price) => progress("billing", `Billing: ${price}`));
|
|
36905
37051
|
if (b.stop)
|
|
36906
37052
|
return { status: "action_required", provider, name, ...b.stop };
|
|
36907
37053
|
({ billing, billingPage, accepted } = b);
|
|
36908
|
-
progress("billing", `Billing: ${billing.price}`);
|
|
36909
37054
|
}
|
|
36910
37055
|
if (opts.resetPassword) {
|
|
37056
|
+
let lock;
|
|
36911
37057
|
try {
|
|
36912
|
-
|
|
37058
|
+
lock = await deps.resetLock(serverOf2(name));
|
|
36913
37059
|
} catch (err) {
|
|
36914
37060
|
if (err instanceof HttpStatusError && err.status === 409) {
|
|
36915
|
-
return { status: "action_required", provider, name, next: `Another pgai connect --reset-password for ${serverOf2(name)} is running: wait for it to finish, then re-run` };
|
|
37061
|
+
return { status: "action_required", provider, name, ...billing, next: `Another pgai connect --reset-password for ${serverOf2(name)} is running: wait for it to finish, then re-run (a run that stopped frees the server 15 minutes after it started)` };
|
|
36916
37062
|
}
|
|
36917
37063
|
if (err instanceof HttpStatusError && err.status === 404) {
|
|
36918
|
-
return { status: "action_required", provider, name, next: `This platform cannot lock the server for --reset-password yet: set PGAI_MON_PASSWORD to ${DEFAULT_MONITORING_USER}'s password instead` };
|
|
37064
|
+
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` };
|
|
36919
37065
|
}
|
|
36920
37066
|
throw err;
|
|
36921
37067
|
}
|
|
37068
|
+
if (typeof lock.lock_id !== "string" || !lock.lock_id)
|
|
37069
|
+
throw new Error("cloud_monitoring_reset_lock returned no lock_id: nothing was changed");
|
|
37070
|
+
lockId = lock.lock_id;
|
|
36922
37071
|
}
|
|
36923
37072
|
let prepared;
|
|
36924
37073
|
let created;
|
|
37074
|
+
let unanswered = false;
|
|
36925
37075
|
try {
|
|
36926
37076
|
if (lockId) {
|
|
36927
37077
|
const now = await deps.list();
|
|
36928
37078
|
const same = now.find((d) => d.name === name && !disconnecting2(d.status));
|
|
36929
37079
|
if (same)
|
|
36930
|
-
return { status: "action_required", provider, name, id: same.id, next: `${name} is already connected, and its monitoring uses the current password: pgai disconnect ${name} --yes first` };
|
|
37080
|
+
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` };
|
|
36931
37081
|
const others = othersOnServer(now);
|
|
36932
37082
|
if (others.length)
|
|
36933
|
-
return cutOff(others);
|
|
37083
|
+
return { ...cutOff(others), ...billing };
|
|
36934
37084
|
}
|
|
36935
37085
|
progress("preparing", `Preparing ${maskConnectionString(url)}`);
|
|
36936
|
-
prepared = await deps.prepare(url, provider,
|
|
37086
|
+
prepared = await deps.prepare(url, provider, prepOpts);
|
|
36937
37087
|
if ("next" in prepared)
|
|
36938
37088
|
return { status: "action_required", provider, name, ...billing, ...prepared };
|
|
37089
|
+
if (!("monitoringUrl" in prepared))
|
|
37090
|
+
throw new Error("prepare returned no monitoring URL");
|
|
36939
37091
|
if (prepared.note)
|
|
36940
37092
|
note = `; ${prepared.note}`;
|
|
36941
37093
|
if (opts.selfHosted) {
|
|
@@ -36952,10 +37104,14 @@ async function connect2(url, opts, deps) {
|
|
|
36952
37104
|
}).catch(async (err) => {
|
|
36953
37105
|
if (err instanceof HttpStatusError && err.status >= 400 && err.status < 500)
|
|
36954
37106
|
await undo(prepared);
|
|
37107
|
+
else
|
|
37108
|
+
unanswered = true;
|
|
36955
37109
|
if (err instanceof HttpStatusError && err.status === 402)
|
|
36956
37110
|
return /declined/i.test(err.message) ? "declined" : "payment";
|
|
36957
37111
|
if (err instanceof HttpStatusError && err.status === 412)
|
|
36958
37112
|
return "price";
|
|
37113
|
+
if (err instanceof HttpStatusError && err.status === 409 && "storedPassword" in prepared && prepared.storedPassword && /none is stored/.test(err.message))
|
|
37114
|
+
return "stored";
|
|
36959
37115
|
throw err;
|
|
36960
37116
|
});
|
|
36961
37117
|
if (created === "payment")
|
|
@@ -36965,18 +37121,26 @@ async function connect2(url, opts, deps) {
|
|
|
36965
37121
|
}
|
|
36966
37122
|
if (created === "price")
|
|
36967
37123
|
return { status: "action_required", provider, name, ...billing, next: "The price changed since it was shown: re-run pgai connect to see it" };
|
|
37124
|
+
if (created === "stored") {
|
|
37125
|
+
const gone = `PostgresAI no longer keeps the password of ${DEFAULT_MONITORING_USER} for this server`;
|
|
37126
|
+
return { status: "action_required", provider, name, ...billing, next: opts.agent ? `${gone}: pass its URL as database_url, or run pgai connect <admin-url> --reset-password in a terminal` : `${gone}: re-run with --reset-password (anything else that logs in as ${DEFAULT_MONITORING_USER} then needs the new one), or set PGAI_MON_PASSWORD to its password` };
|
|
37127
|
+
}
|
|
36968
37128
|
if (created.status === "failed") {
|
|
36969
37129
|
await undo(prepared);
|
|
36970
37130
|
return { status: "failed", provider, name, id: created.id, next: `${created.error} Re-run pgai connect later.` };
|
|
36971
37131
|
}
|
|
36972
37132
|
} finally {
|
|
36973
|
-
if (lockId)
|
|
37133
|
+
if (lockId && !unanswered)
|
|
36974
37134
|
await deps.resetUnlock(lockId).catch(() => {});
|
|
36975
37135
|
}
|
|
36976
37136
|
row = { id: created.id, name: created.name, provider, status: created.status, dashboard_url: null, host_metrics: !!ch };
|
|
36977
|
-
|
|
36978
|
-
|
|
37137
|
+
if (prepared.storedPassword) {
|
|
37138
|
+
progress("checkup", `Express checkup skipped: it runs only as ${DEFAULT_MONITORING_USER}, whose password PostgresAI keeps for this server. The full checkup follows on the box.`);
|
|
37139
|
+
} else {
|
|
37140
|
+
checkup = await deps.checkup(withLocalTls2(prepared.monitoringUrl, url), created.name).catch((err) => ({ error: errorText2(err) }));
|
|
37141
|
+
progress("checkup", checkupLines2(checkup).join(`
|
|
36979
37142
|
`), { checkup });
|
|
37143
|
+
}
|
|
36980
37144
|
}
|
|
36981
37145
|
let shown;
|
|
36982
37146
|
for (const deadline = deps.now() + opts.waitMs;; ) {
|
|
@@ -36996,9 +37160,11 @@ async function connect2(url, opts, deps) {
|
|
|
36996
37160
|
row = (await deps.list().catch(() => [])).find((d) => d.id === row.id) ?? row;
|
|
36997
37161
|
}
|
|
36998
37162
|
}
|
|
36999
|
-
function openConnection2(url, opts) {
|
|
37163
|
+
async function openConnection2(url, opts) {
|
|
37000
37164
|
const conn = resolveAdminConnection({ conn: url });
|
|
37001
|
-
|
|
37165
|
+
const fallback = !opts.agent && !!conn.sslFallbackEnabled;
|
|
37166
|
+
const { client, usedSsl } = await connectWithSslFallback(opts.Client ?? Client2, { ...conn, sslFallbackEnabled: fallback });
|
|
37167
|
+
return { client, usedSsl, refusedTls: fallback && !usedSsl };
|
|
37002
37168
|
}
|
|
37003
37169
|
async function dropMonitoringRole2(client) {
|
|
37004
37170
|
for (const sql of [`drop role ${DEFAULT_MONITORING_USER}`, `drop owned by ${DEFAULT_MONITORING_USER}; drop role ${DEFAULT_MONITORING_USER}`]) {
|
|
@@ -37031,7 +37197,7 @@ async function prepareDatabase2(url, provider, opts = {}) {
|
|
|
37031
37197
|
throw err;
|
|
37032
37198
|
}
|
|
37033
37199
|
};
|
|
37034
|
-
const { client } = await open(url).catch((err) => {
|
|
37200
|
+
const { client, usedSsl, refusedTls } = await open(url).catch((err) => {
|
|
37035
37201
|
if (/client password must be a string/.test(String(err?.message)))
|
|
37036
37202
|
throw new Error("The URL has no password (and PGPASSWORD is not set): pgai connect postgresql://user:password@host:5432/dbname");
|
|
37037
37203
|
throw err;
|
|
@@ -37045,7 +37211,7 @@ async function prepareDatabase2(url, provider, opts = {}) {
|
|
|
37045
37211
|
and has_database_privilege(current_user, current_database(), 'CREATE')
|
|
37046
37212
|
and exists (select 1 from pg_extension where extname = 'pg_stat_statements')) as admin,
|
|
37047
37213
|
exists (select 1 from pg_roles where rolname = '${DEFAULT_MONITORING_USER}') as mon_exists,
|
|
37048
|
-
current_setting('scram_iterations', true) as iterations
|
|
37214
|
+
current_setting('scram_iterations', true) as iterations, current_setting('ssl') as ssl
|
|
37049
37215
|
from pg_roles where rolname = current_user`)).rows[0];
|
|
37050
37216
|
const notes = [caNote2(url)].filter((n) => !!n);
|
|
37051
37217
|
const noted = () => notes.length ? { note: notes.join("; ") } : {};
|
|
@@ -37069,9 +37235,31 @@ async function prepareDatabase2(url, provider, opts = {}) {
|
|
|
37069
37235
|
}
|
|
37070
37236
|
} else if (me.admin && pgProvider !== "supabase") {
|
|
37071
37237
|
const exists = `${DEFAULT_MONITORING_USER} already exists on this server`;
|
|
37238
|
+
const setPassword = `Set PGAI_MON_PASSWORD to the password of ${DEFAULT_MONITORING_USER}, or give it a new one: pgai connect <admin-url> --reset-password (anything else that logs in as ${DEFAULT_MONITORING_USER} then needs the new password)`;
|
|
37239
|
+
const needs = storedPasswordNeeds2(monitoringUrlFor2(url, me.db, ""));
|
|
37240
|
+
const noTls = refusedTls || !usedSsl && me.ssl === "off";
|
|
37241
|
+
const tlsOnly = `PostgresAI sends the password it keeps for ${DEFAULT_MONITORING_USER} only over TLS`;
|
|
37242
|
+
const others = opts.others?.length ? opts.others : undefined;
|
|
37243
|
+
const disconnectOthers = others ? disconnectEach2(others) : "pgai disconnect the server's other databases (pgai databases)";
|
|
37244
|
+
const reconnectOthers = `(and connect ${others ? others.join(", ") : "them"} again with it)`;
|
|
37245
|
+
const noTlsWayOut = opts.agent ? `Pass its URL as database_url, or run pgai connect in a terminal with PGAI_MON_PASSWORD set to its password. If nobody has it, in a terminal: ${disconnectOthers}, then pgai connect <admin-url> --reset-password with PGAI_MON_PASSWORD set to a new one ${reconnectOthers}` : `Set PGAI_MON_PASSWORD to its password, or turn on TLS on the server (ssl = on) and put sslmode=require in the URL. If nobody has the password: ${disconnectOthers}, then re-run with --reset-password and PGAI_MON_PASSWORD set to a new one ${reconnectOthers}`;
|
|
37246
|
+
const reuse = me.mon_exists && !opts.resetPassword && !!opts.storedPassword && !(opts.agent ? "" : process.env.PGAI_MON_PASSWORD?.trim());
|
|
37247
|
+
if (reuse && needs) {
|
|
37248
|
+
if (noTls)
|
|
37249
|
+
return { next: `${exists}, but this server takes no TLS, and ${tlsOnly}. ${noTlsWayOut}` };
|
|
37250
|
+
return { next: `${exists}. ${opts.agent ? "database_url" : "The URL"} needs ${needs}: the password PostgresAI keeps for this server is sent only over TLS, to a URL with each parameter once` };
|
|
37251
|
+
}
|
|
37252
|
+
if (reuse) {
|
|
37253
|
+
if (opts.check)
|
|
37254
|
+
return { checked: true };
|
|
37255
|
+
const { password: unused } = await resolveMonitoringPassword({ monitoringUser: DEFAULT_MONITORING_USER });
|
|
37256
|
+
const plan = await buildInitPlan({ database: me.db, monitoringPassword: unused, iterations: Number(me.iterations), includeOptionalPermissions: true, provider: pgProvider, keepExistingPassword: true });
|
|
37257
|
+
await applyInitPlan({ client, plan });
|
|
37258
|
+
return { monitoringUrl: monitoringUrlFor2(url, me.db, ""), storedPassword: true, ...noted() };
|
|
37259
|
+
}
|
|
37072
37260
|
if (me.mon_exists && opts.agent)
|
|
37073
37261
|
return { next: `${exists}. Pass its URL as database_url, or run pgai connect in a terminal with PGAI_MON_PASSWORD set to its password` };
|
|
37074
|
-
const
|
|
37262
|
+
const unset = !opts.storedPassword ? setPassword : noTls ? `This server takes no TLS, and ${tlsOnly}. ${noTlsWayOut}` : !needs ? `Unset PGAI_MON_PASSWORD: PostgresAI keeps the password of ${DEFAULT_MONITORING_USER} for this server` : `Unset PGAI_MON_PASSWORD, and the URL needs ${needs}: PostgresAI keeps the password of ${DEFAULT_MONITORING_USER} for this server, and sends it only over TLS, to a URL with each parameter once`;
|
|
37075
37263
|
const { password, generated } = await resolveMonitoringPassword({ passwordEnv: opts.agent ? undefined : process.env.PGAI_MON_PASSWORD, monitoringUser: DEFAULT_MONITORING_USER });
|
|
37076
37264
|
const reset = !!opts.resetPassword && me.mon_exists;
|
|
37077
37265
|
if (me.mon_exists && !reset) {
|
|
@@ -37081,13 +37269,16 @@ async function prepareDatabase2(url, provider, opts = {}) {
|
|
|
37081
37269
|
try {
|
|
37082
37270
|
accepted = await logsIn(loginUrlFor2(url, me.db, password));
|
|
37083
37271
|
} catch (err) {
|
|
37084
|
-
|
|
37272
|
+
const orUnset = opts.storedPassword && !noTls ? ", or unset PGAI_MON_PASSWORD: PostgresAI keeps its password for this server" : "";
|
|
37273
|
+
return { next: `${exists}, and PGAI_MON_PASSWORD could not be checked from this host (${err instanceof Error ? err.message : String(err)}). Run pgai connect from a host that ${DEFAULT_MONITORING_USER} may connect from${orUnset}` };
|
|
37085
37274
|
}
|
|
37086
37275
|
if (!accepted)
|
|
37087
|
-
return { next: `${exists} and PGAI_MON_PASSWORD is not its password. ${
|
|
37276
|
+
return { next: `${exists} and PGAI_MON_PASSWORD is not its password. ${unset}` };
|
|
37088
37277
|
if (await acceptsAnyPassword())
|
|
37089
37278
|
notes.push(unchecked("PGAI_MON_PASSWORD"));
|
|
37090
37279
|
}
|
|
37280
|
+
if (opts.check)
|
|
37281
|
+
return { checked: true };
|
|
37091
37282
|
const monitoringUrl = monitoringUrlFor2(url, me.db, password);
|
|
37092
37283
|
const loginUrl = loginUrlFor2(url, me.db, password);
|
|
37093
37284
|
const plan = await buildInitPlan({ database: me.db, monitoringPassword: password, iterations: Number(me.iterations), includeOptionalPermissions: true, provider: pgProvider, keepExistingPassword: !reset });
|
|
@@ -37135,26 +37326,17 @@ async function expressCheckup2(url, opts = {}) {
|
|
|
37135
37326
|
}
|
|
37136
37327
|
}
|
|
37137
37328
|
async function saveCheckupReport2(rpc, accessToken, project, reports) {
|
|
37138
|
-
const
|
|
37139
|
-
const a = answer ?? {};
|
|
37140
|
-
const id = Number(keys.map((k) => a[k]).find((v) => v !== undefined));
|
|
37141
|
-
if (!(id > 0))
|
|
37142
|
-
throw new Error(typeof a.message === "string" ? a.message : `Unexpected ${fn} answer: ${JSON.stringify(answer)}`);
|
|
37143
|
-
return id;
|
|
37144
|
-
};
|
|
37145
|
-
const reportId = idOf("checkup_report_create", await rpc("checkup_report_create", { access_token: accessToken, project }), "report_id");
|
|
37329
|
+
const { reportId } = await createCheckupReport(rpc, { apiKey: accessToken, project });
|
|
37146
37330
|
const setStatus = (status) => rpc("checkup_report_status_update", { access_token: accessToken, report_id: reportId, status });
|
|
37147
37331
|
try {
|
|
37148
37332
|
for (const [checkId, report] of Object.entries(reports)) {
|
|
37149
|
-
|
|
37150
|
-
|
|
37151
|
-
|
|
37333
|
+
await uploadCheckupReportJson(rpc, {
|
|
37334
|
+
apiKey: accessToken,
|
|
37335
|
+
reportId,
|
|
37152
37336
|
filename: `${checkId}.json`,
|
|
37153
|
-
|
|
37154
|
-
|
|
37155
|
-
|
|
37156
|
-
generate_issue: true
|
|
37157
|
-
}), "report_chunck_id", "report_chunk_id");
|
|
37337
|
+
checkId,
|
|
37338
|
+
jsonText: JSON.stringify(report, null, 2)
|
|
37339
|
+
});
|
|
37158
37340
|
}
|
|
37159
37341
|
} catch (err) {
|
|
37160
37342
|
await setStatus("failed").catch(() => {});
|
|
@@ -37192,6 +37374,17 @@ async function clickhouseOrgFor2(host, keyId, keySecret) {
|
|
|
37192
37374
|
}
|
|
37193
37375
|
throw failed ?? notFound;
|
|
37194
37376
|
}
|
|
37377
|
+
function disconnectBilling(reply) {
|
|
37378
|
+
const r = reply ?? {};
|
|
37379
|
+
if (r.billing_warning)
|
|
37380
|
+
return `not released (${r.billing_warning}): contact support`;
|
|
37381
|
+
if (r.billing?.subscription === "canceled")
|
|
37382
|
+
return "subscription canceled: no further charges; the unused part of this period is credited (prorated)";
|
|
37383
|
+
if (r.billing?.subscription === "active" && typeof r.billing.quantity === "number") {
|
|
37384
|
+
return `${r.billing.quantity} ${r.billing.quantity === 1 ? "database cluster" : "database clusters"} left on the subscription`;
|
|
37385
|
+
}
|
|
37386
|
+
return;
|
|
37387
|
+
}
|
|
37195
37388
|
function platformDeps2(p) {
|
|
37196
37389
|
const rpc = (fn, body = {}) => callRpc({ apiKey: p.apiKey, apiBaseUrl: p.apiBaseUrl, fn, body, operation: fn.replace(/_/g, " "), debug: p.debug, orgScope: p.orgScope });
|
|
37197
37390
|
return {
|
|
@@ -37204,7 +37397,7 @@ function platformDeps2(p) {
|
|
|
37204
37397
|
unprepare: (url) => unprepareDatabase2(url, { agent: p.agent }),
|
|
37205
37398
|
clickhouseOrg: clickhouseOrgFor2,
|
|
37206
37399
|
checkup: (url, project) => expressCheckup2(url, { agent: p.agent, save: (reports) => saveCheckupReport2(rpc, p.apiKey, project, reports) }),
|
|
37207
|
-
quote: (coupon) => rpc("cloud_monitoring_quote", coupon ? { promo_code: coupon } : {}),
|
|
37400
|
+
quote: (coupon, cluster) => rpc("cloud_monitoring_quote", { ...coupon ? { promo_code: coupon } : {}, ...cluster ? { db_server: cluster } : {} }),
|
|
37208
37401
|
billingUrl: (orgAlias) => `${p.uiBaseUrl}/${orgAlias}/billing`,
|
|
37209
37402
|
handoffUrl: async (provider) => {
|
|
37210
37403
|
const orgs = await listOrgs({ apiKey: p.apiKey, apiBaseUrl: p.apiBaseUrl });
|
|
@@ -39329,10 +39522,6 @@ async function applyUninitPlan(params) {
|
|
|
39329
39522
|
}
|
|
39330
39523
|
return { applied, errors };
|
|
39331
39524
|
}
|
|
39332
|
-
async function legacyHelperWarnings(client) {
|
|
39333
|
-
const r = await client.query("select to_regprocedure('postgres_ai.explain_generic(text,text,text)') is not null as present");
|
|
39334
|
-
return r.rows?.[0]?.present ? ["legacy SECURITY DEFINER helper postgres_ai.explain_generic(text, text, text) is still installed; its owner or a superuser must drop it"] : [];
|
|
39335
|
-
}
|
|
39336
39525
|
async function verifyInitSetup2(params) {
|
|
39337
39526
|
await params.client.query("begin isolation level repeatable read;");
|
|
39338
39527
|
try {
|
|
@@ -40497,7 +40686,7 @@ function feedbackMessage() {
|
|
|
40497
40686
|
}
|
|
40498
40687
|
|
|
40499
40688
|
// bin/postgres-ai.ts
|
|
40500
|
-
import { createInterface } from "readline";
|
|
40689
|
+
import { createInterface as createInterface2 } from "readline";
|
|
40501
40690
|
import { Writable } from "stream";
|
|
40502
40691
|
import * as childProcess from "child_process";
|
|
40503
40692
|
|
|
@@ -40505,7 +40694,7 @@ import * as childProcess from "child_process";
|
|
|
40505
40694
|
import * as fs9 from "fs";
|
|
40506
40695
|
import * as path8 from "path";
|
|
40507
40696
|
var __dirname = "/builds/postgres-ai/postgresai/cli/lib";
|
|
40508
|
-
var CONTRACT_VERSION2 = "1.
|
|
40697
|
+
var CONTRACT_VERSION2 = "1.0.0";
|
|
40509
40698
|
var SECONDS_PER_DAY2 = 86400;
|
|
40510
40699
|
var SECONDS_PER_HOUR2 = 3600;
|
|
40511
40700
|
var SECONDS_PER_MINUTE2 = 60;
|
|
@@ -43090,48 +43279,11 @@ async function verifyApiKey(params) {
|
|
|
43090
43279
|
clearTimeout(timer);
|
|
43091
43280
|
}
|
|
43092
43281
|
}
|
|
43093
|
-
async function
|
|
43094
|
-
|
|
43095
|
-
const bodyObj = {
|
|
43096
|
-
access_token: apiKey,
|
|
43097
|
-
project
|
|
43098
|
-
};
|
|
43099
|
-
if (status)
|
|
43100
|
-
bodyObj.status = status;
|
|
43101
|
-
const resp = await postRpc({
|
|
43102
|
-
apiKey,
|
|
43103
|
-
apiBaseUrl,
|
|
43104
|
-
rpcName: "checkup_report_create",
|
|
43105
|
-
bodyObj
|
|
43106
|
-
});
|
|
43107
|
-
const reportId = Number(resp?.report_id);
|
|
43108
|
-
if (!Number.isFinite(reportId) || reportId <= 0) {
|
|
43109
|
-
throw new Error(`Unexpected checkup_report_create response: ${JSON.stringify(resp)}`);
|
|
43110
|
-
}
|
|
43111
|
-
return { reportId };
|
|
43282
|
+
async function createCheckupReport2(params) {
|
|
43283
|
+
return createCheckupReport((rpcName, bodyObj) => postRpc({ ...params, rpcName, bodyObj }), params);
|
|
43112
43284
|
}
|
|
43113
|
-
async function
|
|
43114
|
-
|
|
43115
|
-
const bodyObj = {
|
|
43116
|
-
access_token: apiKey,
|
|
43117
|
-
checkup_report_id: reportId,
|
|
43118
|
-
filename,
|
|
43119
|
-
check_id: checkId,
|
|
43120
|
-
data: jsonText,
|
|
43121
|
-
type: "json",
|
|
43122
|
-
generate_issue: true
|
|
43123
|
-
};
|
|
43124
|
-
const resp = await postRpc({
|
|
43125
|
-
apiKey,
|
|
43126
|
-
apiBaseUrl,
|
|
43127
|
-
rpcName: "checkup_report_file_post",
|
|
43128
|
-
bodyObj
|
|
43129
|
-
});
|
|
43130
|
-
const chunkId = Number(resp?.report_chunck_id ?? resp?.report_chunk_id);
|
|
43131
|
-
if (!Number.isFinite(chunkId) || chunkId <= 0) {
|
|
43132
|
-
throw new Error(`Unexpected checkup_report_file_post response: ${JSON.stringify(resp)}`);
|
|
43133
|
-
}
|
|
43134
|
-
return { reportChunkId: chunkId };
|
|
43285
|
+
async function uploadCheckupReportJson2(params) {
|
|
43286
|
+
return uploadCheckupReportJson((rpcName, bodyObj) => postRpc({ ...params, rpcName, bodyObj }), params);
|
|
43135
43287
|
}
|
|
43136
43288
|
async function convertCheckupReportJsonToMarkdown(params) {
|
|
43137
43289
|
const { apiKey, apiBaseUrl, checkId, jsonPayload, reportType } = params;
|
|
@@ -43525,21 +43677,45 @@ function summarizeG0032(nodeData) {
|
|
|
43525
43677
|
}
|
|
43526
43678
|
|
|
43527
43679
|
// lib/json-stderr.ts
|
|
43680
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
43681
|
+
import { createInterface } from "node:readline";
|
|
43528
43682
|
import { format } from "node:util";
|
|
43529
43683
|
function writeEvent(event) {
|
|
43530
43684
|
process.stderr.write(`${JSON.stringify(event)}
|
|
43531
43685
|
`);
|
|
43532
43686
|
}
|
|
43533
|
-
|
|
43687
|
+
function jsonConsole() {
|
|
43534
43688
|
const { error, warn } = console;
|
|
43535
|
-
|
|
43536
|
-
|
|
43537
|
-
|
|
43538
|
-
|
|
43539
|
-
|
|
43689
|
+
const log = (level) => (...args) => {
|
|
43690
|
+
const message = format(...args);
|
|
43691
|
+
writeEvent({ event: "log", level: /^\s*Debug:/.test(message) ? "debug" : /^\s*(?:\[[^\]]+\]\s*)?Warning:/.test(message) ? "warn" : level, message });
|
|
43692
|
+
};
|
|
43693
|
+
console.error = log("error");
|
|
43694
|
+
console.warn = log("warn");
|
|
43695
|
+
return () => {
|
|
43540
43696
|
console.error = error;
|
|
43541
43697
|
console.warn = warn;
|
|
43542
|
-
}
|
|
43698
|
+
};
|
|
43699
|
+
}
|
|
43700
|
+
var LOGIN = /^(\s*(?:Login|VictoriaMetrics Auth): ).+$/;
|
|
43701
|
+
function runChild(command, args, env, json, source) {
|
|
43702
|
+
if (!json)
|
|
43703
|
+
return Promise.resolve(spawnSync(command, args, { stdio: ["ignore", 2, 2], env }).status);
|
|
43704
|
+
return new Promise((resolve) => {
|
|
43705
|
+
const child = spawn(command, args, { stdio: ["ignore", "pipe", "pipe"], env });
|
|
43706
|
+
for (const [stream, level] of [[child.stdout, "info"], [child.stderr, "error"]]) {
|
|
43707
|
+
createInterface({ input: stream }).on("line", (line) => {
|
|
43708
|
+
const message = line.replace(LOGIN, "$1***** (pgai mon show-grafana-credentials)");
|
|
43709
|
+
if (line.trim() !== "")
|
|
43710
|
+
writeEvent({ event: "log", level, source, message });
|
|
43711
|
+
});
|
|
43712
|
+
}
|
|
43713
|
+
child.on("error", (err) => {
|
|
43714
|
+
writeEvent({ event: "log", level: "error", source, message: err.message });
|
|
43715
|
+
resolve(null);
|
|
43716
|
+
});
|
|
43717
|
+
child.on("close", (code) => resolve(code));
|
|
43718
|
+
});
|
|
43543
43719
|
}
|
|
43544
43720
|
|
|
43545
43721
|
// lib/instances.ts
|
|
@@ -43687,12 +43863,22 @@ function buildClientConfig(connStr, extra = {}) {
|
|
|
43687
43863
|
port: parsed.port ? Number(parsed.port) : undefined,
|
|
43688
43864
|
user: parsed.user,
|
|
43689
43865
|
password: parsed.password,
|
|
43690
|
-
database: parsed.database
|
|
43866
|
+
database: databaseName3(withoutSslmode(channelBinding.uri), parsed.database),
|
|
43691
43867
|
ssl: sslOptionFromSslmode(sslmode),
|
|
43692
43868
|
...enableChannelBinding ? { enableChannelBinding: true } : {},
|
|
43693
43869
|
...extra
|
|
43694
43870
|
};
|
|
43695
43871
|
}
|
|
43872
|
+
function databaseName3(connStr, parsed) {
|
|
43873
|
+
if (!parsed)
|
|
43874
|
+
return;
|
|
43875
|
+
try {
|
|
43876
|
+
const u = new URL(connStr);
|
|
43877
|
+
if (/^postgres(ql)?:$/.test(u.protocol))
|
|
43878
|
+
return decodeURIComponent(u.pathname.slice(1));
|
|
43879
|
+
} catch {}
|
|
43880
|
+
return parsed;
|
|
43881
|
+
}
|
|
43696
43882
|
function withoutSslmode(connStr) {
|
|
43697
43883
|
try {
|
|
43698
43884
|
const u = new URL(connStr);
|
|
@@ -43736,7 +43922,7 @@ Download: https://nodejs.org/`);
|
|
|
43736
43922
|
var rl = null;
|
|
43737
43923
|
function getReadline() {
|
|
43738
43924
|
if (!rl) {
|
|
43739
|
-
rl =
|
|
43925
|
+
rl = createInterface2({ input: process.stdin, output: process.stdout });
|
|
43740
43926
|
}
|
|
43741
43927
|
return rl;
|
|
43742
43928
|
}
|
|
@@ -43961,7 +44147,7 @@ async function execFilePromise(file, args, options = {}) {
|
|
|
43961
44147
|
});
|
|
43962
44148
|
});
|
|
43963
44149
|
}
|
|
43964
|
-
function
|
|
44150
|
+
function spawnSync3(cmd, args, options) {
|
|
43965
44151
|
const result = childProcess.spawnSync(cmd, args, {
|
|
43966
44152
|
stdio: options?.stdio === "inherit" ? "inherit" : "pipe",
|
|
43967
44153
|
env: options?.env,
|
|
@@ -43974,7 +44160,7 @@ function spawnSync2(cmd, args, options) {
|
|
|
43974
44160
|
stderr: typeof result.stderr === "string" ? result.stderr : ""
|
|
43975
44161
|
};
|
|
43976
44162
|
}
|
|
43977
|
-
function
|
|
44163
|
+
function spawn3(cmd, args, options) {
|
|
43978
44164
|
const proc = childProcess.spawn(cmd, args, {
|
|
43979
44165
|
stdio: options?.stdio ?? "pipe",
|
|
43980
44166
|
env: options?.env,
|
|
@@ -44005,7 +44191,7 @@ async function question(prompt) {
|
|
|
44005
44191
|
}
|
|
44006
44192
|
async function questionHidden(prompt) {
|
|
44007
44193
|
closeReadline();
|
|
44008
|
-
const hidden =
|
|
44194
|
+
const hidden = createInterface2({ input: process.stdin, output: new Writable({ write: (_chunk, _encoding, done) => done() }), terminal: true });
|
|
44009
44195
|
process.stdout.write(prompt);
|
|
44010
44196
|
return new Promise((resolve) => {
|
|
44011
44197
|
hidden.question("", (answer) => {
|
|
@@ -44136,7 +44322,7 @@ function prepareUploadConfig(opts, rootOpts, shouldUpload, uploadExplicitlyReque
|
|
|
44136
44322
|
}
|
|
44137
44323
|
async function uploadCheckupReports(uploadCfg, reports, spinner, logUpload) {
|
|
44138
44324
|
spinner.update("Creating remote checkup report");
|
|
44139
|
-
const created = await withRetry(() =>
|
|
44325
|
+
const created = await withRetry(() => createCheckupReport2({
|
|
44140
44326
|
apiKey: uploadCfg.apiKey,
|
|
44141
44327
|
apiBaseUrl: uploadCfg.apiBaseUrl,
|
|
44142
44328
|
project: uploadCfg.project
|
|
@@ -44149,7 +44335,7 @@ async function uploadCheckupReports(uploadCfg, reports, spinner, logUpload) {
|
|
|
44149
44335
|
for (const [checkId, report] of Object.entries(reports)) {
|
|
44150
44336
|
spinner.update(`Uploading ${checkId}.json`);
|
|
44151
44337
|
const jsonText = JSON.stringify(report, null, 2);
|
|
44152
|
-
const r = await withRetry(() =>
|
|
44338
|
+
const r = await withRetry(() => uploadCheckupReportJson2({
|
|
44153
44339
|
apiKey: uploadCfg.apiKey,
|
|
44154
44340
|
apiBaseUrl: uploadCfg.apiBaseUrl,
|
|
44155
44341
|
reportId,
|
|
@@ -44290,47 +44476,12 @@ function sanitizeTagForBackup(tag) {
|
|
|
44290
44476
|
const stripped = stripMatchingQuotes(tag);
|
|
44291
44477
|
return /^[A-Za-z0-9._-]{1,64}$/.test(stripped) ? stripped : null;
|
|
44292
44478
|
}
|
|
44293
|
-
function
|
|
44294
|
-
const semver = (v) => {
|
|
44295
|
-
const m = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/.exec(v);
|
|
44296
|
-
return m ? { core: [Number(m[1]), Number(m[2]), Number(m[3])], pre: m[4] } : null;
|
|
44297
|
-
};
|
|
44298
|
-
const cli = semver(cliVersion);
|
|
44299
|
-
if (!cli || cli.pre) {
|
|
44300
|
-
return { tag: null, note: `PGAI_TAG stays ${current ?? "unset"}: this CLI (${cliVersion}) is not a release. To pick a stack version, set PGAI_TAG=<version> in .env and re-run 'postgresai mon update'` };
|
|
44301
|
-
}
|
|
44302
|
-
if (current === cliVersion)
|
|
44303
|
-
return { tag: null, note: `PGAI_TAG is ${cliVersion}, matching this CLI` };
|
|
44304
|
-
const cur = current ? semver(current) : null;
|
|
44305
|
-
if (cur) {
|
|
44306
|
-
const i = cur.core.findIndex((part, j) => part !== cli.core[j]);
|
|
44307
|
-
if (i >= 0 && cur.core[i] > cli.core[i]) {
|
|
44308
|
-
return { tag: null, note: `PGAI_TAG stays ${current}: it is newer than this CLI (${cliVersion}). Upgrade the CLI to move the stack: npm install -g postgresai@latest` };
|
|
44309
|
-
}
|
|
44310
|
-
}
|
|
44311
|
-
return { tag: cliVersion, note: `PGAI_TAG: ${current ?? "unset"} -> ${cliVersion}` };
|
|
44312
|
-
}
|
|
44313
|
-
var ENV_TAG_LINE = /^([ \t]*(?:export[ \t]+)?PGAI_TAG[ \t]*=[ \t]*)(["']?)([A-Za-z0-9._-]*)\2((?:[ \t]+#[^\r\n]*)?[ \t]*\r?)$/gm;
|
|
44314
|
-
var ENV_TAG_ANY = /^[ \t]*(?:export[ \t]+)?PGAI_TAG[ \t]*[=:]/gm;
|
|
44315
|
-
function readEnvTag(projectDir) {
|
|
44479
|
+
function readDeployedTag(projectDir) {
|
|
44316
44480
|
const envFile = path9.resolve(projectDir, ".env");
|
|
44317
44481
|
if (!fs11.existsSync(envFile))
|
|
44318
|
-
return
|
|
44319
|
-
const
|
|
44320
|
-
|
|
44321
|
-
if (plainLines.length !== [...content.matchAll(ENV_TAG_ANY)].length)
|
|
44322
|
-
return { tag: null, plain: false };
|
|
44323
|
-
const last = plainLines.at(-1);
|
|
44324
|
-
return { tag: last && last[3] ? last[3] : null, plain: true };
|
|
44325
|
-
}
|
|
44326
|
-
function writeEnvTag(projectDir, tag) {
|
|
44327
|
-
const envFile = path9.resolve(projectDir, ".env");
|
|
44328
|
-
const existing = fs11.existsSync(envFile) ? fs11.readFileSync(envFile, "utf8") : "";
|
|
44329
|
-
const content = new RegExp(ENV_TAG_LINE.source, "m").test(existing) ? existing.replace(ENV_TAG_LINE, (_match, prefix, quote, _value, rest) => `${prefix}${quote}${tag}${quote}${rest}`) : `${existing}${existing && !existing.endsWith(`
|
|
44330
|
-
`) ? `
|
|
44331
|
-
` : ""}PGAI_TAG=${tag}
|
|
44332
|
-
`;
|
|
44333
|
-
writeEnvFile(envFile, content);
|
|
44482
|
+
return null;
|
|
44483
|
+
const m = fs11.readFileSync(envFile, "utf8").match(/^PGAI_TAG=(.+)$/m);
|
|
44484
|
+
return m ? sanitizeTagForBackup(m[1]) : null;
|
|
44334
44485
|
}
|
|
44335
44486
|
function isValidComposeYaml(text) {
|
|
44336
44487
|
const trimmed = text.trim();
|
|
@@ -44381,7 +44532,7 @@ async function refreshBundledComposeIfStale(projectDir, oldTag) {
|
|
|
44381
44532
|
const existing = fs11.readFileSync(composeFile, "utf8");
|
|
44382
44533
|
if (composeContentEqual(existing, fetched))
|
|
44383
44534
|
return false;
|
|
44384
|
-
const deployedTag = sanitizeTagForBackup(oldTag ??
|
|
44535
|
+
const deployedTag = sanitizeTagForBackup(oldTag ?? readDeployedTag(projectDir));
|
|
44385
44536
|
const tagPart = deployedTag ?? new Date().toISOString().replace(/[:.]/g, "-");
|
|
44386
44537
|
const oldHash = crypto2.createHash("sha256").update(existing).digest("hex").slice(0, 8);
|
|
44387
44538
|
const backup = path9.resolve(projectDir, `docker-compose.yml.bak-${tagPart}-${oldHash}`);
|
|
@@ -44431,6 +44582,56 @@ function printResult(result, json) {
|
|
|
44431
44582
|
var program2 = new Command2;
|
|
44432
44583
|
program2.name("postgres-ai").description("PostgresAI CLI").version(package_default.version).option("--api-key <key>", "API key (overrides PGAI_API_KEY)").option("--api-base-url <url>", "API base URL for backend RPC (overrides PGAI_API_BASE_URL)").option("--ui-base-url <url>", "UI base URL for browser routes (overrides PGAI_UI_BASE_URL)").option("--storage-base-url <url>", "Storage base URL for file uploads (overrides PGAI_STORAGE_BASE_URL)");
|
|
44433
44584
|
program2.showHelpAfterError();
|
|
44585
|
+
program2.configureOutput({
|
|
44586
|
+
outputError: (text, write) => jsonConnectArgv() ? writeEvent({ event: "log", level: "error", message: text.trim().replace(/^(error: unknown option ')(--[^=]*|-[^-])[\s\S]*'(?=\n\(Did you mean [^\n]*\)$|$)/, "$1$2'") }) : write(text),
|
|
44587
|
+
writeErr: (text) => void (jsonConnectArgv() || process.stderr.write(text))
|
|
44588
|
+
});
|
|
44589
|
+
function splitArgv(options, argv) {
|
|
44590
|
+
const operands = [];
|
|
44591
|
+
const unknown = [];
|
|
44592
|
+
let dest = operands;
|
|
44593
|
+
let json = false;
|
|
44594
|
+
for (let i = 0;i < argv.length; i++) {
|
|
44595
|
+
const arg = argv[i];
|
|
44596
|
+
if (arg === "--") {
|
|
44597
|
+
dest.push(...dest === unknown ? argv.slice(i) : argv.slice(i + 1));
|
|
44598
|
+
break;
|
|
44599
|
+
}
|
|
44600
|
+
const option = options.find((o) => o.long === arg || o.short === arg);
|
|
44601
|
+
if (option) {
|
|
44602
|
+
if (option.required)
|
|
44603
|
+
i++;
|
|
44604
|
+
else if (option.long === "--json")
|
|
44605
|
+
json = true;
|
|
44606
|
+
continue;
|
|
44607
|
+
}
|
|
44608
|
+
if (options.some((o) => o.required && arg.startsWith(`${o.long}=`)))
|
|
44609
|
+
continue;
|
|
44610
|
+
if (dest === operands && arg.length > 1 && arg.startsWith("-"))
|
|
44611
|
+
dest = unknown;
|
|
44612
|
+
dest.push(arg);
|
|
44613
|
+
}
|
|
44614
|
+
return { operands, unknown, json };
|
|
44615
|
+
}
|
|
44616
|
+
function argvRun(argv = process.argv.slice(2)) {
|
|
44617
|
+
const root = splitArgv(program2.options, argv);
|
|
44618
|
+
let command = program2.commands.find((c) => c.name() === root.operands[0]);
|
|
44619
|
+
let rest = root.unknown;
|
|
44620
|
+
if (root.operands.length === 0) {
|
|
44621
|
+
const end = rest.indexOf("--");
|
|
44622
|
+
const at = rest.slice(0, end < 0 ? undefined : end).indexOf("connect");
|
|
44623
|
+
if (at >= 0)
|
|
44624
|
+
[command, rest] = [program2.commands.find((c) => c.name() === "connect"), rest.slice(at + 1)];
|
|
44625
|
+
}
|
|
44626
|
+
return { command, json: !!command && splitArgv(command.options, rest).json };
|
|
44627
|
+
}
|
|
44628
|
+
function jsonConnect(command, json) {
|
|
44629
|
+
return command?.parent === program2 && command.name() === "connect" && jsonOutput(json);
|
|
44630
|
+
}
|
|
44631
|
+
function jsonConnectArgv() {
|
|
44632
|
+
const run = argvRun();
|
|
44633
|
+
return jsonConnect(run.command, run.json);
|
|
44634
|
+
}
|
|
44434
44635
|
program2.addHelpText("after", () => `
|
|
44435
44636
|
\uD83D\uDCA1 Ideas / feedback: ${FEEDBACK_URL}
|
|
44436
44637
|
`);
|
|
@@ -44452,9 +44653,15 @@ function withOrgOptions(command) {
|
|
|
44452
44653
|
ORG_SCOPED_COMMANDS.add(command);
|
|
44453
44654
|
return command.option("--org <alias>", `organization alias (or ${ORG_ENV}); required with a global token`).option("--org-id <id>", `organization id (or ${ORG_ID_ENV}); alternative to --org`);
|
|
44454
44655
|
}
|
|
44455
|
-
|
|
44456
|
-
|
|
44457
|
-
|
|
44656
|
+
var restoreConsole;
|
|
44657
|
+
program2.hook("preAction", (_thisCommand, actionCommand) => {
|
|
44658
|
+
if (jsonConnect(actionCommand, actionCommand.opts().json))
|
|
44659
|
+
restoreConsole = jsonConsole();
|
|
44660
|
+
});
|
|
44661
|
+
program2.hook("postAction", () => {
|
|
44662
|
+
restoreConsole?.();
|
|
44663
|
+
restoreConsole = undefined;
|
|
44664
|
+
});
|
|
44458
44665
|
program2.hook("preAction", (_thisCommand, actionCommand) => {
|
|
44459
44666
|
const initPointsToConnect = actionCommand.parent === program2 && actionCommand.name() === "init" && !interactive(actionCommand.opts().json);
|
|
44460
44667
|
if (!ORG_SCOPED_COMMANDS.has(actionCommand) || initPointsToConnect) {
|
|
@@ -44467,10 +44674,7 @@ program2.hook("preAction", (_thisCommand, actionCommand) => {
|
|
|
44467
44674
|
setActiveOrgScope(requireOrgScope(opts, apiKey));
|
|
44468
44675
|
} catch (err) {
|
|
44469
44676
|
if (err instanceof OrgScopeError2) {
|
|
44470
|
-
|
|
44471
|
-
writeEvent({ event: "log", level: "error", message: err.message });
|
|
44472
|
-
else
|
|
44473
|
-
console.error(err.message);
|
|
44677
|
+
console.error(err.message);
|
|
44474
44678
|
process.exit(1);
|
|
44475
44679
|
}
|
|
44476
44680
|
throw err;
|
|
@@ -44485,7 +44689,7 @@ program2.command("feedback").description("share ideas or feedback about the CLI"
|
|
|
44485
44689
|
if (opts.open) {
|
|
44486
44690
|
const openCommand = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
44487
44691
|
try {
|
|
44488
|
-
|
|
44692
|
+
spawn3(openCommand, [FEEDBACK_URL], { detached: true, stdio: "ignore" }).unref();
|
|
44489
44693
|
} catch {}
|
|
44490
44694
|
}
|
|
44491
44695
|
});
|
|
@@ -44692,7 +44896,6 @@ program2.command("prepare-db [conn]").description("prepare database for monitori
|
|
|
44692
44896
|
monitoringUser: opts.monitoringUser,
|
|
44693
44897
|
includeOptionalPermissions
|
|
44694
44898
|
});
|
|
44695
|
-
v.missingOptional.push(...await legacyHelperWarnings(supabaseClient));
|
|
44696
44899
|
if (v.ok) {
|
|
44697
44900
|
if (jsonOutput) {
|
|
44698
44901
|
const result = {
|
|
@@ -44815,7 +45018,6 @@ program2.command("prepare-db [conn]").description("prepare database for monitori
|
|
|
44815
45018
|
client: supabaseClient,
|
|
44816
45019
|
plan: effectivePlan
|
|
44817
45020
|
});
|
|
44818
|
-
const legacyWarnings = await legacyHelperWarnings(supabaseClient);
|
|
44819
45021
|
if (jsonOutput) {
|
|
44820
45022
|
const result = {
|
|
44821
45023
|
success: true,
|
|
@@ -44825,10 +45027,7 @@ program2.command("prepare-db [conn]").description("prepare database for monitori
|
|
|
44825
45027
|
monitoringUser: opts.monitoringUser,
|
|
44826
45028
|
applied,
|
|
44827
45029
|
skippedOptional,
|
|
44828
|
-
warnings: [
|
|
44829
|
-
...skippedOptional.length > 0 ? ["Some optional steps were skipped (not supported or insufficient privileges)"] : [],
|
|
44830
|
-
...legacyWarnings
|
|
44831
|
-
]
|
|
45030
|
+
warnings: skippedOptional.length > 0 ? ["Some optional steps were skipped (not supported or insufficient privileges)"] : []
|
|
44832
45031
|
};
|
|
44833
45032
|
if (passwordGenerated) {
|
|
44834
45033
|
result.generatedPassword = monPassword;
|
|
@@ -44844,8 +45043,6 @@ program2.command("prepare-db [conn]").description("prepare database for monitori
|
|
|
44844
45043
|
for (const s of skippedOptional)
|
|
44845
45044
|
console.error(`- ${s}`);
|
|
44846
45045
|
}
|
|
44847
|
-
for (const w of legacyWarnings)
|
|
44848
|
-
console.error(`\u26A0 ${w}`);
|
|
44849
45046
|
if (process.stdout.isTTY) {
|
|
44850
45047
|
console.log(`Applied ${applied.length} steps`);
|
|
44851
45048
|
}
|
|
@@ -44987,7 +45184,6 @@ program2.command("prepare-db [conn]").description("prepare database for monitori
|
|
|
44987
45184
|
includeOptionalPermissions,
|
|
44988
45185
|
provider
|
|
44989
45186
|
});
|
|
44990
|
-
v.missingOptional.push(...await legacyHelperWarnings(client));
|
|
44991
45187
|
if (v.ok) {
|
|
44992
45188
|
if (jsonOutput) {
|
|
44993
45189
|
outputJson({
|
|
@@ -45106,7 +45302,6 @@ program2.command("prepare-db [conn]").description("prepare database for monitori
|
|
|
45106
45302
|
return;
|
|
45107
45303
|
}
|
|
45108
45304
|
const { applied, skippedOptional } = await applyInitPlan2({ client, plan: effectivePlan });
|
|
45109
|
-
const legacyWarnings = await legacyHelperWarnings(client);
|
|
45110
45305
|
if (jsonOutput) {
|
|
45111
45306
|
const result = {
|
|
45112
45307
|
success: true,
|
|
@@ -45116,10 +45311,7 @@ program2.command("prepare-db [conn]").description("prepare database for monitori
|
|
|
45116
45311
|
monitoringUser: opts.monitoringUser,
|
|
45117
45312
|
applied,
|
|
45118
45313
|
skippedOptional,
|
|
45119
|
-
warnings: [
|
|
45120
|
-
...skippedOptional.length > 0 ? ["Some optional steps were skipped (not supported or insufficient privileges)"] : [],
|
|
45121
|
-
...legacyWarnings
|
|
45122
|
-
]
|
|
45314
|
+
warnings: skippedOptional.length > 0 ? ["Some optional steps were skipped (not supported or insufficient privileges)"] : []
|
|
45123
45315
|
};
|
|
45124
45316
|
if (passwordGenerated) {
|
|
45125
45317
|
result.generatedPassword = monPassword;
|
|
@@ -45132,8 +45324,6 @@ program2.command("prepare-db [conn]").description("prepare database for monitori
|
|
|
45132
45324
|
for (const s of skippedOptional)
|
|
45133
45325
|
console.error(`- ${s}`);
|
|
45134
45326
|
}
|
|
45135
|
-
for (const w of legacyWarnings)
|
|
45136
|
-
console.error(`\u26A0 ${w}`);
|
|
45137
45327
|
if (process.stdout.isTTY) {
|
|
45138
45328
|
console.log(`Applied ${applied.length} steps`);
|
|
45139
45329
|
}
|
|
@@ -45784,7 +45974,7 @@ function ensureHostMetricsDir(projectDir) {
|
|
|
45784
45974
|
}
|
|
45785
45975
|
function isDockerRunning() {
|
|
45786
45976
|
try {
|
|
45787
|
-
const result =
|
|
45977
|
+
const result = spawnSync3("docker", ["info"], { stdio: "pipe", timeout: 5000 });
|
|
45788
45978
|
return result.status === 0;
|
|
45789
45979
|
} catch {
|
|
45790
45980
|
return false;
|
|
@@ -45797,7 +45987,7 @@ function shouldScaleOutNodeExporter(args) {
|
|
|
45797
45987
|
return !args.slice(upIndex + 1).some((a) => !a.startsWith("-"));
|
|
45798
45988
|
}
|
|
45799
45989
|
function getComposeCmd() {
|
|
45800
|
-
const tryCmd = (cmd, args) =>
|
|
45990
|
+
const tryCmd = (cmd, args) => spawnSync3(cmd, args, { stdio: "ignore", timeout: 5000 }).status === 0;
|
|
45801
45991
|
if (tryCmd("docker", ["compose", "version"]))
|
|
45802
45992
|
return ["docker", "compose"];
|
|
45803
45993
|
if (tryCmd("docker-compose", ["version"]))
|
|
@@ -45806,7 +45996,7 @@ function getComposeCmd() {
|
|
|
45806
45996
|
}
|
|
45807
45997
|
function checkRunningContainers() {
|
|
45808
45998
|
try {
|
|
45809
|
-
const result =
|
|
45999
|
+
const result = spawnSync3("docker", ["ps", "--filter", "name=grafana-with-datasources", "--filter", "name=pgwatch", "--format", "{{.Names}}"], { stdio: "pipe", encoding: "utf8", timeout: 5000 });
|
|
45810
46000
|
if (result.status === 0 && result.stdout) {
|
|
45811
46001
|
const containers = result.stdout.trim().split(`
|
|
45812
46002
|
`).filter(Boolean);
|
|
@@ -46151,7 +46341,7 @@ async function runCompose(args, grafanaPassword, extraEnv) {
|
|
|
46151
46341
|
finalArgs.push("--scale", "self-node-exporter=0");
|
|
46152
46342
|
}
|
|
46153
46343
|
return new Promise((resolve) => {
|
|
46154
|
-
const child =
|
|
46344
|
+
const child = spawn3(cmd[0], [...cmd.slice(1), "-f", composeFile, ...finalArgs], {
|
|
46155
46345
|
stdio: "inherit",
|
|
46156
46346
|
env,
|
|
46157
46347
|
cwd: projectDir
|
|
@@ -46341,7 +46531,7 @@ function emitConnect(result, json) {
|
|
|
46341
46531
|
process.exitCode = CONNECT_EXIT[result.status];
|
|
46342
46532
|
}
|
|
46343
46533
|
function failCloud(err, json) {
|
|
46344
|
-
printResult({ status: "failed", next:
|
|
46534
|
+
printResult({ status: "failed", next: errorText2(err) }, json);
|
|
46345
46535
|
process.exitCode = 1;
|
|
46346
46536
|
}
|
|
46347
46537
|
function signedIn(opts) {
|
|
@@ -46352,22 +46542,8 @@ function signedIn(opts) {
|
|
|
46352
46542
|
}
|
|
46353
46543
|
return !!cloudApi().apiKey;
|
|
46354
46544
|
}
|
|
46355
|
-
function runConnect(url, opts) {
|
|
46356
|
-
|
|
46357
|
-
}
|
|
46358
|
-
function runLocalInstall(args, env, json) {
|
|
46359
|
-
if (!json)
|
|
46360
|
-
return Promise.resolve(childProcess.spawnSync(process.execPath, args, { stdio: ["ignore", 2, 2], env }).status);
|
|
46361
|
-
const child = childProcess.spawn(process.execPath, args, { stdio: ["ignore", "pipe", "pipe"], env });
|
|
46362
|
-
for (const stream of [child.stdout, child.stderr]) {
|
|
46363
|
-
createInterface({ input: stream }).on("line", (line) => writeEvent({ event: "log", level: "info", source: "mon local-install", message: line }));
|
|
46364
|
-
}
|
|
46365
|
-
return new Promise((resolve) => {
|
|
46366
|
-
child.on("error", () => resolve(null));
|
|
46367
|
-
child.on("close", (code) => resolve(code));
|
|
46368
|
-
});
|
|
46369
|
-
}
|
|
46370
|
-
async function connectCommand(url, opts, json) {
|
|
46545
|
+
async function runConnect(url, opts) {
|
|
46546
|
+
const json = jsonOutput(opts.json);
|
|
46371
46547
|
const name = (() => {
|
|
46372
46548
|
try {
|
|
46373
46549
|
return databaseName2(url);
|
|
@@ -46394,7 +46570,7 @@ async function connectCommand(url, opts, json) {
|
|
|
46394
46570
|
const scope = getActiveOrgScope2();
|
|
46395
46571
|
const org = scope?.alias ? ["--org", scope.alias] : scope?.id ? ["--org-id", String(scope.id)] : [];
|
|
46396
46572
|
const project = name.replace(/[^A-Za-z0-9._-]+/g, "-");
|
|
46397
|
-
const status = await
|
|
46573
|
+
const status = await runChild(process.execPath, [process.argv[1], "mon", "local-install", "-y", ...org, "--project", project], { ...process.env, ...env, PGAI_DB_URL: monitoringUrl, PGAI_API_BASE_URL: api.apiBaseUrl, ...api.apiKey ? { PGAI_API_KEY: api.apiKey } : {} }, json, "mon local-install");
|
|
46398
46574
|
if (status !== 0)
|
|
46399
46575
|
throw new Error("mon local-install failed (see above)");
|
|
46400
46576
|
},
|
|
@@ -46412,31 +46588,32 @@ async function connectCommand(url, opts, json) {
|
|
|
46412
46588
|
});
|
|
46413
46589
|
emitConnect(result, opts.json);
|
|
46414
46590
|
} catch (err) {
|
|
46415
|
-
emitConnect({ status: "failed", provider, name, next:
|
|
46591
|
+
emitConnect({ status: "failed", provider, name, next: errorText2(err) }, opts.json);
|
|
46416
46592
|
}
|
|
46417
46593
|
}
|
|
46418
|
-
withOrgOptions(program2.command("connect <database-url>")).
|
|
46419
|
-
writeErr: (text) => {
|
|
46420
|
-
if (!(process.argv.includes("--json") || !process.stdout.isTTY))
|
|
46421
|
-
return void process.stderr.write(text);
|
|
46422
|
-
for (const line of text.split(`
|
|
46423
|
-
`).filter((l) => l.trim() !== ""))
|
|
46424
|
-
writeEvent({ event: "log", level: "error", message: line });
|
|
46425
|
-
}
|
|
46426
|
-
}).description("put a database under PostgresAI's care: prepare it, provision monitoring, print the dashboard").option("--provider <provider>", "clickhouse | rds | supabase | self-managed (default: detected from the host)").option("--clickhouse-key <id:secret>", "ClickHouse Cloud API key (Basic Service API Reader) for CPU, memory and disk; or CLICKHOUSE_KEY_ID + CLICKHOUSE_KEY_SECRET").option("--self-hosted", "run the monitoring stack on this machine (mon local-install) instead of PostgresAI Cloud").option("--reset-password", "postgres_ai_mon exists and its password is lost: set a new one (admin URL; refused while another database on the server is monitored)").option("--wait <minutes>", "how long to wait for the monitoring box (0 = do not wait)", "20").option("--coupon <code>", "promotion code for the organization's monitoring subscription (applies when it is first created)").option("-y, --yes", "never prompt: accept the price of a billed box").option("--json", "JSON output (default when stdout is not a TTY)").option("--debug", "print HTTP requests (secrets masked)").addHelpText("after", [
|
|
46594
|
+
withOrgOptions(program2.command("connect <database-url>")).description("put a database under PostgresAI's care: prepare it, provision monitoring, print the dashboard").option("--provider <provider>", "clickhouse | rds | supabase | self-managed (default: detected from the host)").option("--clickhouse-key <id:secret>", "ClickHouse Cloud API key (Basic Service API Reader) for CPU, memory and disk; or CLICKHOUSE_KEY_ID + CLICKHOUSE_KEY_SECRET").option("--self-hosted", "run the monitoring stack on this machine (mon local-install) instead of PostgresAI Cloud").option("--reset-password", "postgres_ai_mon exists and its password is lost: set a new one (admin URL; refused while another database on the server is monitored)").option("--wait <minutes>", "how long to wait for the monitoring box (0 = do not wait)", "20").option("--coupon <code>", "promotion code for the organization's monitoring subscription (applies when it is first created)").option("-y, --yes", "never prompt: accept the price of a billed box").option("--json", "JSON output (default when stdout is not a TTY)").option("--debug", "print HTTP requests (secrets masked)").addHelpText("after", [
|
|
46427
46595
|
"",
|
|
46428
46596
|
"Steps (each skipped when already done; safe to re-run): sign in, prepare the database",
|
|
46429
46597
|
"(an admin URL creates the postgres_ai_mon role; otherwise the SQL is printed), provision",
|
|
46430
46598
|
"the monitoring box, run the express checkup while it starts, wait, print the dashboard URL.",
|
|
46431
46599
|
"",
|
|
46432
|
-
|
|
46600
|
+
"Billing is per Postgres cluster observed: another database in a cluster the organization already",
|
|
46601
|
+
'monitors is included. Once the URL is checked, connect shows the price (or "free (N of M free slots)"). A billed',
|
|
46433
46602
|
"box is provisioned only when accepted: at the prompt, or with --yes. With no payment method",
|
|
46434
46603
|
"it stops (exit 3) and names the console page to add one. --coupon applies a promotion code.",
|
|
46435
46604
|
"",
|
|
46436
46605
|
'Exit codes: 0 connected or provisioning, 1 failed, 3 action required (see "next").',
|
|
46437
46606
|
"",
|
|
46607
|
+
"JSON output (--json, or stdout not a TTY): stdout is the result; stderr is one JSON event a",
|
|
46608
|
+
'line: the steps, and {"event":"log","level":"error"|"warn"|"info"|"debug","message":...}',
|
|
46609
|
+
'for any other text (an error, a warning, --debug); "source":"mon local-install" for that',
|
|
46610
|
+
"child's lines (the logins it prints are masked: pgai mon show-grafana-credentials shows them).",
|
|
46611
|
+
"",
|
|
46438
46612
|
"Environment: PGAI_API_KEY (instead of signing in), PGAI_MON_PASSWORD (the password of",
|
|
46439
|
-
"postgres_ai_mon when the role already exists; it is checked, never changed
|
|
46613
|
+
"postgres_ai_mon when the role already exists; it is checked, never changed; not needed",
|
|
46614
|
+
"for another database on a server PostgresAI already monitors for the org, with",
|
|
46615
|
+
"sslmode=require or verify-* in the URL: PostgresAI fills in the password it keeps (over TLS only), and",
|
|
46616
|
+
"the express checkup is skipped, as it runs only as postgres_ai_mon),",
|
|
46440
46617
|
"PGPASSWORD (the password for a URL without one),",
|
|
46441
46618
|
"CLICKHOUSE_KEY_ID + CLICKHOUSE_KEY_SECRET (instead of --clickhouse-key).",
|
|
46442
46619
|
"",
|
|
@@ -46447,6 +46624,9 @@ withOrgOptions(program2.command("connect <database-url>")).configureOutput({
|
|
|
46447
46624
|
].join(`
|
|
46448
46625
|
`)).action(runConnect);
|
|
46449
46626
|
var interactive = (json) => !!process.stdin.isTTY && !!process.stdout.isTTY && !json;
|
|
46627
|
+
function jsonOutput(json) {
|
|
46628
|
+
return !!json || !process.stdout.isTTY;
|
|
46629
|
+
}
|
|
46450
46630
|
withOrgOptions(program2.command("init")).description("first run for a person at a terminal: sign in, ask for the database URL, then pgai connect").option("--json", "JSON output (init is interactive: prints the pgai connect command to use instead)").action(async (opts) => {
|
|
46451
46631
|
if (!interactive(opts.json)) {
|
|
46452
46632
|
return emitConnect({ status: "action_required", provider: "self-managed", name: "", next: "pgai init is for a person at a terminal; agents and scripts: pgai connect <database-url>" }, opts.json);
|
|
@@ -46489,8 +46669,8 @@ withOrgOptions(program2.command("disconnect <name>")).description("stop monitori
|
|
|
46489
46669
|
if (!opts.yes && !(process.stdin.isTTY && /^y/i.test(await question(`Disconnect ${row.name} and delete its monitoring box? (y/N): `)))) {
|
|
46490
46670
|
return emitConnect({ status: "action_required", provider: row.provider, name: row.name, id: row.id, next: `pgai disconnect ${row.name} --yes` }, opts.json);
|
|
46491
46671
|
}
|
|
46492
|
-
await cloudApi(opts.debug).disconnect(row.id);
|
|
46493
|
-
printResult({ status: "disconnected", name: row.name, id: row.id, next: row.host_metrics ? "Delete the ClickHouse Cloud API key you gave us" : "none" }, opts.json);
|
|
46672
|
+
const billing = disconnectBilling(await cloudApi(opts.debug).disconnect(row.id));
|
|
46673
|
+
printResult({ status: "disconnected", name: row.name, id: row.id, ...billing ? { billing } : {}, next: row.host_metrics ? "Delete the ClickHouse Cloud API key you gave us" : "none" }, opts.json);
|
|
46494
46674
|
} catch (err) {
|
|
46495
46675
|
failCloud(err, opts.json);
|
|
46496
46676
|
}
|
|
@@ -47076,9 +47256,9 @@ mon.command("health").description("health check for monitoring services").option
|
|
|
47076
47256
|
allHealthy = true;
|
|
47077
47257
|
for (const service of services) {
|
|
47078
47258
|
try {
|
|
47079
|
-
const result =
|
|
47259
|
+
const result = spawnSync3("docker", ["inspect", "-f", "{{.State.Status}}", service.container], { stdio: "pipe" });
|
|
47080
47260
|
const present = result.status === 0;
|
|
47081
|
-
const health = present && service.readHealth ? (
|
|
47261
|
+
const health = present && service.readHealth ? (spawnSync3("docker", ["inspect", "-f", "{{if .State.Health}}{{.State.Health.Status}}{{end}}", service.container], { stdio: "pipe" }).stdout || "").trim() : "";
|
|
47082
47262
|
const verdict = judgeServiceHealth(service, present, (result.stdout || "").trim(), health);
|
|
47083
47263
|
console.log(`${verdict.mark} ${service.name}: ${verdict.label}`);
|
|
47084
47264
|
if (!verdict.ok)
|
|
@@ -47172,18 +47352,6 @@ mon.command("update").description("update monitoring stack (migrate .env, pull i
|
|
|
47172
47352
|
} else {
|
|
47173
47353
|
console.log("\u2713 .env is up to date");
|
|
47174
47354
|
}
|
|
47175
|
-
const envTag = readEnvTag(projectDir);
|
|
47176
|
-
const deployedTag = envTag.tag;
|
|
47177
|
-
const tagPlan = envTag.plain ? planUpdateTag(deployedTag, package_default.version) : { tag: null, note: `PGAI_TAG in .env is not a plain value, so it is left as is. To move the stack to ${package_default.version}, set PGAI_TAG=${package_default.version} in .env and re-run 'postgresai mon update'` };
|
|
47178
|
-
if (process.env.PGAI_TAG && process.env.PGAI_TAG !== (tagPlan.tag ?? deployedTag)) {
|
|
47179
|
-
console.error(`\u26A0 PGAI_TAG=${process.env.PGAI_TAG} is set in the environment, and docker compose prefers it over .env: unset it before 'postgresai mon stop && postgresai mon start'`);
|
|
47180
|
-
}
|
|
47181
|
-
if (tagPlan.tag) {
|
|
47182
|
-
writeEnvTag(projectDir, tagPlan.tag);
|
|
47183
|
-
if (process.env.PGAI_TAG === deployedTag)
|
|
47184
|
-
process.env.PGAI_TAG = tagPlan.tag;
|
|
47185
|
-
}
|
|
47186
|
-
console.log(`${tagPlan.tag ? "\u2713 " : ""}${tagPlan.note}`);
|
|
47187
47355
|
console.log();
|
|
47188
47356
|
const gitDir = path9.resolve(projectDir, ".git");
|
|
47189
47357
|
if (fs11.existsSync(gitDir)) {
|
|
@@ -47197,7 +47365,7 @@ mon.command("update").description("update monitoring stack (migrate .env, pull i
|
|
|
47197
47365
|
console.log(pullOut);
|
|
47198
47366
|
} else {
|
|
47199
47367
|
console.log("(not a git checkout \u2014 checking bundled docker-compose.yml)");
|
|
47200
|
-
await refreshBundledComposeIfStale(projectDir
|
|
47368
|
+
await refreshBundledComposeIfStale(projectDir);
|
|
47201
47369
|
}
|
|
47202
47370
|
console.log(`
|
|
47203
47371
|
Updating Docker images...`);
|
|
@@ -47217,7 +47385,7 @@ Updating Docker images...`);
|
|
|
47217
47385
|
\u2713 Update completed successfully`);
|
|
47218
47386
|
console.log(`
|
|
47219
47387
|
To apply updates, restart monitoring services:`);
|
|
47220
|
-
console.log("
|
|
47388
|
+
console.log(" postgres-ai mon restart");
|
|
47221
47389
|
} else {
|
|
47222
47390
|
console.error(`
|
|
47223
47391
|
\u2717 Docker image update failed`);
|
|
@@ -47358,7 +47526,7 @@ async function sinkPrometheusMaybeUp() {
|
|
|
47358
47526
|
} catch {
|
|
47359
47527
|
return true;
|
|
47360
47528
|
}
|
|
47361
|
-
const ps =
|
|
47529
|
+
const ps = spawnSync3(cmd[0], [...cmd.slice(1), "-f", composeFile, "ps", "-a", "-q", "--status", "running", "--status", "paused", "sink-prometheus"]);
|
|
47362
47530
|
return ps.status !== 0 || ps.stdout.trim() !== "";
|
|
47363
47531
|
}
|
|
47364
47532
|
async function reloadHostMetrics(projectDir, job, file) {
|
|
@@ -47393,14 +47561,36 @@ async function reloadHostMetrics(projectDir, job, file) {
|
|
|
47393
47561
|
}
|
|
47394
47562
|
return true;
|
|
47395
47563
|
}
|
|
47564
|
+
var CONN_STR_FORMAT = "Invalid connection string format: use postgresql://user:password@host[:port]/database";
|
|
47396
47565
|
function defaultTargetName(connStr) {
|
|
47566
|
+
if (!/^postgres(ql)?:\/\//.test(connStr))
|
|
47567
|
+
return { error: CONN_STR_FORMAT };
|
|
47568
|
+
if (/[\x00-\x1f\x7f]/.test(connStr))
|
|
47569
|
+
return { error: "Invalid connection string format: remove the control character (such as a CR from a CRLF file), or percent-encode it" };
|
|
47570
|
+
const at = connStr.lastIndexOf("@");
|
|
47571
|
+
if (at < 0)
|
|
47572
|
+
return { error: CONN_STR_FORMAT };
|
|
47573
|
+
if (!/^(?:[A-Za-z0-9\-._~!$&'()*+,;=:@]|%[0-9A-Fa-f]{2})*$/.test(connStr.slice(connStr.indexOf("//") + 2, at))) {
|
|
47574
|
+
return { error: "Invalid connection string format: percent-encode the user name and the password (all but ASCII letters, digits, '-', '.', '_' and '~'), and an '@' in the database name or the query" };
|
|
47575
|
+
}
|
|
47576
|
+
let url;
|
|
47397
47577
|
try {
|
|
47398
|
-
new URL(connStr);
|
|
47578
|
+
url = new URL(connStr);
|
|
47399
47579
|
} catch {
|
|
47400
|
-
return
|
|
47401
|
-
}
|
|
47402
|
-
|
|
47403
|
-
|
|
47580
|
+
return { error: CONN_STR_FORMAT };
|
|
47581
|
+
}
|
|
47582
|
+
if (!url.username)
|
|
47583
|
+
return { error: CONN_STR_FORMAT };
|
|
47584
|
+
if (/%3a/i.test(url.username))
|
|
47585
|
+
return { error: "Invalid connection string format: percent-encode the user name and the password one at a time, with a raw ':' between them" };
|
|
47586
|
+
if (!url.password)
|
|
47587
|
+
return { error: "Invalid connection string format: put the password in the URL: postgresql://user:password@host[:port]/database" };
|
|
47588
|
+
if (url.hostname.includes(":"))
|
|
47589
|
+
return { error: "Invalid connection string format: an IPv6 address is not supported as the host; use a host name" };
|
|
47590
|
+
const db = url.pathname.slice(1);
|
|
47591
|
+
if (!db)
|
|
47592
|
+
return { error: CONN_STR_FORMAT };
|
|
47593
|
+
return { name: `${url.hostname}-${db}`.replace(/[^a-zA-Z0-9-]/g, "-") };
|
|
47404
47594
|
}
|
|
47405
47595
|
function parseEnvValue(content, key) {
|
|
47406
47596
|
const last = [...content.matchAll(new RegExp(`^[ \\t]*(?:export[ \\t]+)?${key}=(.*)$`, "gm"))].at(-1);
|
|
@@ -47445,7 +47635,7 @@ async function setRdsHostStats(projectDir, values) {
|
|
|
47445
47635
|
if (!cmd)
|
|
47446
47636
|
return true;
|
|
47447
47637
|
const { composeFile } = await resolveOrInitPaths();
|
|
47448
|
-
const ps =
|
|
47638
|
+
const ps = spawnSync3(cmd[0], [...cmd.slice(1), "-f", composeFile, "--profile", "rds", "ps", "-q", "--status", "running", "rds-host-stats"]);
|
|
47449
47639
|
if (ps.status !== 0) {
|
|
47450
47640
|
console.error("If rds-host-stats is running, recreate it: docker compose --profile rds up -d rds-host-stats");
|
|
47451
47641
|
return true;
|
|
@@ -47530,6 +47720,7 @@ async function syncVmalert(projectDir, apply, addOnly = false) {
|
|
|
47530
47720
|
return true;
|
|
47531
47721
|
}
|
|
47532
47722
|
var looksLikeConnStr = (value) => /^postgres(ql)?:\/\//i.test(value);
|
|
47723
|
+
var isPlainTargetName = (value) => /^[A-Za-z0-9._=-]+$/.test(value) && !/(password|pwd)\s*=/i.test(value);
|
|
47533
47724
|
async function addTarget(file, projectDir, connStr, name, env, { apply = true } = {}) {
|
|
47534
47725
|
if (!connStr) {
|
|
47535
47726
|
console.error("Connection string required: postgresql://user:pass@host:port/db");
|
|
@@ -47539,15 +47730,15 @@ async function addTarget(file, projectDir, connStr, name, env, { apply = true }
|
|
|
47539
47730
|
const channelBinding = splitChannelBinding2(connStr);
|
|
47540
47731
|
connStr = channelBinding.uri;
|
|
47541
47732
|
const defaultName = defaultTargetName(connStr);
|
|
47542
|
-
if (
|
|
47543
|
-
console.error(
|
|
47733
|
+
if ("error" in defaultName) {
|
|
47734
|
+
console.error(defaultName.error);
|
|
47544
47735
|
process.exitCode = 1;
|
|
47545
47736
|
return false;
|
|
47546
47737
|
}
|
|
47547
47738
|
if (channelBinding.value !== null) {
|
|
47548
47739
|
console.error("Note: removed channel_binding from the connection string; the collector does not support it (TLS is kept)");
|
|
47549
47740
|
}
|
|
47550
|
-
const instanceName = name && name.trim() ? name.trim() : defaultName;
|
|
47741
|
+
const instanceName = name && name.trim() ? name.trim() : defaultName.name;
|
|
47551
47742
|
if (detectProvider(connStr) !== "clickhouse" && extractProjectRefFromUrl(connStr) && supabaseHostMetricsOn(projectDir, env) && !SUPABASE_TARGET_NAME_RE.test(instanceName)) {
|
|
47552
47743
|
console.error(SUPABASE_TARGET_NAME_ERROR);
|
|
47553
47744
|
process.exitCode = 1;
|
|
@@ -47630,14 +47821,40 @@ Supabase: with PGAI_SUPABASE_HOST_METRICS=true (environment or .env), writes hos
|
|
|
47630
47821
|
RDS instance endpoint: writes RDS_DB_INSTANCE_IDENTIFIER, AWS_REGION, PGAI_CLUSTER and PGAI_NODE_NAME to .env for rds-host-stats.
|
|
47631
47822
|
|
|
47632
47823
|
Environment:
|
|
47633
|
-
PGAI_DB_URL the connection string,
|
|
47634
|
-
the only argument is then the name
|
|
47824
|
+
PGAI_DB_URL the connection string, unless a postgres:// or postgresql:// URL is given
|
|
47825
|
+
(keeps the password out of argv). The only argument is then the name, of
|
|
47826
|
+
ASCII letters, digits, '.', '_', '=' and '-', with no password= or pwd=;
|
|
47827
|
+
anything else is refused. Set it for this command only:
|
|
47828
|
+
PGAI_DB_URL='postgresql://user:pass@host:5432/db' postgres-ai mon targets add my-db
|
|
47829
|
+
Under sudo, pass it on stdin: sudo logs a variable kept with --preserve-env.
|
|
47830
|
+
printf '%s\\n' "$URL" | sudo sh -c \\
|
|
47831
|
+
'IFS= read -r PGAI_DB_URL; export PGAI_DB_URL; exec postgres-ai mon targets add my-db'
|
|
47832
|
+
sudo I/O logging (log_input in sudoers) records stdin: then read it from a
|
|
47833
|
+
file of mode 0600 in the root shell.
|
|
47834
|
+
sudo sh -c \\
|
|
47835
|
+
'IFS= read -r PGAI_DB_URL < /path/to/db-url; export PGAI_DB_URL; exec postgres-ai mon targets add my-db'
|
|
47635
47836
|
`).action(async (connStr, name) => {
|
|
47636
47837
|
const { instancesFile: file, projectDir } = await resolveOrInitPaths();
|
|
47637
47838
|
const envUrl = process.env.PGAI_DB_URL || undefined;
|
|
47839
|
+
delete process.env.PGAI_DB_URL;
|
|
47638
47840
|
if (envUrl && name === undefined && (connStr === undefined || !looksLikeConnStr(connStr))) {
|
|
47639
|
-
|
|
47841
|
+
const arg = connStr?.trim();
|
|
47842
|
+
if (arg && !isPlainTargetName(arg)) {
|
|
47843
|
+
console.error("PGAI_DB_URL is set: pass only the target name (ASCII letters, digits, '.', '_', '=', '-'; no password= or pwd=)");
|
|
47844
|
+
process.exitCode = 1;
|
|
47845
|
+
return;
|
|
47846
|
+
}
|
|
47847
|
+
name = arg;
|
|
47640
47848
|
connStr = envUrl;
|
|
47849
|
+
let user = "";
|
|
47850
|
+
if (!("error" in defaultTargetName(splitChannelBinding2(envUrl).uri))) {
|
|
47851
|
+
try {
|
|
47852
|
+
user = decodeURIComponent(new URL(envUrl).username);
|
|
47853
|
+
} catch {
|
|
47854
|
+
user = new URL(envUrl).username;
|
|
47855
|
+
}
|
|
47856
|
+
}
|
|
47857
|
+
console.error(`Using PGAI_DB_URL${user ? ` (user ${user})` : ""}`);
|
|
47641
47858
|
}
|
|
47642
47859
|
await addTarget(file, projectDir, connStr, name, process.env);
|
|
47643
47860
|
});
|
|
@@ -47838,7 +48055,7 @@ Opening browser for authentication...`);
|
|
|
47838
48055
|
${authUrl}
|
|
47839
48056
|
`);
|
|
47840
48057
|
const openCommand = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
47841
|
-
const browserProcess =
|
|
48058
|
+
const browserProcess = spawn3(openCommand, [authUrl], { detached: true, stdio: "ignore" });
|
|
47842
48059
|
browserProcess.on("error", () => {});
|
|
47843
48060
|
browserProcess.unref();
|
|
47844
48061
|
console.log("Waiting for authorization...");
|
|
@@ -49547,10 +49764,9 @@ export {
|
|
|
49547
49764
|
monitoringHealthServices,
|
|
49548
49765
|
persistInstanceId,
|
|
49549
49766
|
planMonitoringRegistration,
|
|
49550
|
-
planUpdateTag,
|
|
49551
49767
|
program2 as program,
|
|
49552
49768
|
readComposeProfiles,
|
|
49553
|
-
|
|
49769
|
+
readDeployedTag,
|
|
49554
49770
|
refreshBundledComposeIfStale,
|
|
49555
49771
|
registerMonitoringInstance,
|
|
49556
49772
|
resolveAdoptedProject,
|
|
@@ -49558,6 +49774,5 @@ export {
|
|
|
49558
49774
|
selfRegisterAndPersist,
|
|
49559
49775
|
shouldScaleOutNodeExporter,
|
|
49560
49776
|
statOwner,
|
|
49561
|
-
updatePgwatchConfig
|
|
49562
|
-
writeEnvTag
|
|
49777
|
+
updatePgwatchConfig
|
|
49563
49778
|
};
|