@stacksjs/ts-cloud 0.7.92 → 0.7.94
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/dist/bin/cli.js +1235 -1183
- package/dist/bin/dashboard-server.js +451 -399
- package/dist/{chunk-29fwqkb7.js → chunk-0htae1c2.js} +456 -21
- package/dist/{chunk-fbwcv2vf.js → chunk-bs3jfnzp.js} +451 -41
- package/dist/control-plane/store.d.ts +1 -0
- package/dist/deploy/dashboard-database.d.ts +91 -3
- package/dist/deploy/dashboard-vitess.d.ts +154 -0
- package/dist/deploy/dashboard-vitess.test.d.ts +1 -0
- package/dist/deploy/index.js +2 -2
- package/dist/drivers/hetzner/client.d.ts +96 -0
- package/dist/drivers/hetzner/migrate-server.d.ts +124 -0
- package/dist/drivers/hetzner/migrate-server.test.d.ts +1 -0
- package/dist/drivers/hetzner/role-swap.d.ts +139 -0
- package/dist/drivers/hetzner/role-swap.test.d.ts +1 -0
- package/dist/drivers/index.js +1 -1
- package/dist/drivers/shared/db-provision.d.ts +4 -0
- package/dist/drivers/shared/deploy-script.d.ts +17 -1
- package/dist/drivers/shared/package-manager.d.ts +2 -0
- package/dist/drivers/shared/releases.d.ts +41 -2
- package/dist/drivers/shared/vitess-provision.d.ts +133 -0
- package/dist/drivers/shared/vitess-provision.test.d.ts +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +6 -2
- package/dist/telemetry/store.d.ts +1 -0
- package/dist/ui/account/automation.html +4 -4
- package/dist/ui/account/security.html +2 -2
- package/dist/ui/applications/compose.html +4 -4
- package/dist/ui/data/backups.html +4 -4
- package/dist/ui/data/services.html +4 -4
- package/dist/ui/data/volumes.html +3 -3
- package/dist/ui/index.html +4 -4
- package/dist/ui/integrations.html +2 -2
- package/dist/ui/operations/alerts.html +4 -4
- package/dist/ui/operations/configuration.html +4 -4
- package/dist/ui/operations/jobs.html +4 -4
- package/dist/ui/operations/maintenance.html +3 -3
- package/dist/ui/operations/observability.html +4 -4
- package/dist/ui/operations/previews.html +4 -4
- package/dist/ui/operations/queue.html +4 -4
- package/dist/ui/operations/regions.html +4 -4
- package/dist/ui/operations/releases.html +4 -4
- package/dist/ui/operations/workloads.html +4 -4
- package/dist/ui/security.html +2 -2
- package/dist/ui/server/actions.html +4 -4
- package/dist/ui/server/activity.html +2 -2
- package/dist/ui/server/capacity.html +4 -4
- package/dist/ui/server/database.html +124 -8
- package/dist/ui/server/deployments.html +4 -4
- package/dist/ui/server/diagnostics.html +2 -2
- package/dist/ui/server/firewall.html +4 -4
- package/dist/ui/server/fleet.html +4 -4
- package/dist/ui/server/logs.html +4 -4
- package/dist/ui/server/metrics.html +4 -4
- package/dist/ui/server/services.html +2 -2
- package/dist/ui/server/sites.html +4 -4
- package/dist/ui/server/ssh-keys.html +4 -4
- package/dist/ui/server/team.html +4 -4
- package/dist/ui/server/terminal.html +2 -2
- package/dist/ui/serverless/alarms.html +4 -4
- package/dist/ui/serverless/data.html +3 -3
- package/dist/ui/serverless/firewall.html +2 -2
- package/dist/ui/serverless/functions.html +4 -4
- package/dist/ui/serverless/logs.html +4 -4
- package/dist/ui/serverless/queues.html +4 -4
- package/dist/ui/serverless/secrets.html +4 -4
- package/dist/ui/serverless/traces.html +4 -4
- package/dist/ui/serverless.html +3 -3
- package/dist/ui-src/pages/server/database.stx +257 -9
- package/package.json +3 -3
|
@@ -45,7 +45,7 @@ import {
|
|
|
45
45
|
resolveSiteKind,
|
|
46
46
|
resolveUiSource,
|
|
47
47
|
siteInstallBase
|
|
48
|
-
} from "./chunk-
|
|
48
|
+
} from "./chunk-bs3jfnzp.js";
|
|
49
49
|
import {
|
|
50
50
|
artifactKey,
|
|
51
51
|
buildCloudFormationTemplate,
|
|
@@ -92,7 +92,7 @@ import {
|
|
|
92
92
|
import { createHash as createHash5 } from "node:crypto";
|
|
93
93
|
|
|
94
94
|
// src/control-plane/migrations.ts
|
|
95
|
-
var CONTROL_PLANE_SCHEMA_VERSION =
|
|
95
|
+
var CONTROL_PLANE_SCHEMA_VERSION = 36;
|
|
96
96
|
var controlPlaneMigrations = [
|
|
97
97
|
{
|
|
98
98
|
version: 1,
|
|
@@ -1506,11 +1506,56 @@ var controlPlaneMigrations = [
|
|
|
1506
1506
|
CREATE INDEX cleanup_plan_status ON cleanup_plans(project_id,status,expires_at);
|
|
1507
1507
|
CREATE INDEX dr_drill_status ON disaster_recovery_drills(project_id,status,created_at DESC);
|
|
1508
1508
|
`
|
|
1509
|
+
},
|
|
1510
|
+
{
|
|
1511
|
+
version: 36,
|
|
1512
|
+
name: "telemetry_source_rollups",
|
|
1513
|
+
sql: `
|
|
1514
|
+
CREATE TABLE telemetry_source_rollups (
|
|
1515
|
+
project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
1516
|
+
environment_id TEXT NOT NULL DEFAULT '',
|
|
1517
|
+
resource_id TEXT NOT NULL DEFAULT '',
|
|
1518
|
+
source TEXT NOT NULL,
|
|
1519
|
+
first_observed_at TEXT NOT NULL,
|
|
1520
|
+
latest_observed_at TEXT NOT NULL,
|
|
1521
|
+
ingested_bytes INTEGER NOT NULL DEFAULT 0 CHECK (ingested_bytes >= 0),
|
|
1522
|
+
record_count INTEGER NOT NULL DEFAULT 0 CHECK (record_count >= 0),
|
|
1523
|
+
PRIMARY KEY(project_id, environment_id, resource_id, source)
|
|
1524
|
+
) STRICT;
|
|
1525
|
+
|
|
1526
|
+
INSERT INTO telemetry_source_rollups (
|
|
1527
|
+
project_id, environment_id, resource_id, source,
|
|
1528
|
+
first_observed_at, latest_observed_at, ingested_bytes, record_count
|
|
1529
|
+
)
|
|
1530
|
+
SELECT
|
|
1531
|
+
project_id, COALESCE(environment_id, ''), COALESCE(resource_id, ''), source,
|
|
1532
|
+
MIN(observed_at), MAX(observed_at), SUM(ingested_bytes), COUNT(*)
|
|
1533
|
+
FROM telemetry_records
|
|
1534
|
+
GROUP BY project_id, COALESCE(environment_id, ''), COALESCE(resource_id, ''), source;
|
|
1535
|
+
|
|
1536
|
+
CREATE TRIGGER telemetry_source_rollups_insert
|
|
1537
|
+
AFTER INSERT ON telemetry_records
|
|
1538
|
+
BEGIN
|
|
1539
|
+
INSERT INTO telemetry_source_rollups (
|
|
1540
|
+
project_id, environment_id, resource_id, source,
|
|
1541
|
+
first_observed_at, latest_observed_at, ingested_bytes, record_count
|
|
1542
|
+
)
|
|
1543
|
+
VALUES (
|
|
1544
|
+
NEW.project_id, COALESCE(NEW.environment_id, ''), COALESCE(NEW.resource_id, ''), NEW.source,
|
|
1545
|
+
NEW.observed_at, NEW.observed_at, NEW.ingested_bytes, 1
|
|
1546
|
+
)
|
|
1547
|
+
ON CONFLICT(project_id, environment_id, resource_id, source) DO UPDATE SET
|
|
1548
|
+
first_observed_at = MIN(first_observed_at, excluded.first_observed_at),
|
|
1549
|
+
latest_observed_at = MAX(latest_observed_at, excluded.latest_observed_at),
|
|
1550
|
+
ingested_bytes = ingested_bytes + excluded.ingested_bytes,
|
|
1551
|
+
record_count = record_count + 1;
|
|
1552
|
+
END;
|
|
1553
|
+
`
|
|
1509
1554
|
}
|
|
1510
1555
|
];
|
|
1511
1556
|
// src/control-plane/store.ts
|
|
1512
1557
|
import { createHash, randomBytes } from "node:crypto";
|
|
1513
|
-
import { chmodSync, existsSync, mkdirSync, statSync
|
|
1558
|
+
import { chmodSync, existsSync, mkdirSync, statSync } from "node:fs";
|
|
1514
1559
|
import { dirname } from "node:path";
|
|
1515
1560
|
import { Database } from "bun:sqlite";
|
|
1516
1561
|
|
|
@@ -1978,6 +2023,12 @@ class ControlPlaneStore {
|
|
|
1978
2023
|
} catch {}
|
|
1979
2024
|
}
|
|
1980
2025
|
}
|
|
2026
|
+
backupTo(path) {
|
|
2027
|
+
this.database.run("VACUUM INTO ?", [path]);
|
|
2028
|
+
try {
|
|
2029
|
+
chmodSync(path, 384);
|
|
2030
|
+
} catch {}
|
|
2031
|
+
}
|
|
1981
2032
|
migrate() {
|
|
1982
2033
|
const row = this.database.query("PRAGMA user_version").get();
|
|
1983
2034
|
const current = Number(row?.user_version ?? 0);
|
|
@@ -1988,7 +2039,7 @@ class ControlPlaneStore {
|
|
|
1988
2039
|
let backupPath;
|
|
1989
2040
|
if (current > 0 && this.path !== ":memory:") {
|
|
1990
2041
|
backupPath = `${this.path}.v${current}.${Date.now()}.bak`;
|
|
1991
|
-
|
|
2042
|
+
this.backupTo(backupPath);
|
|
1992
2043
|
}
|
|
1993
2044
|
try {
|
|
1994
2045
|
const apply = this.database.transaction(() => {
|
|
@@ -2263,7 +2314,7 @@ class ControlPlaneStore {
|
|
|
2263
2314
|
return row ? mapMembership(row) : undefined;
|
|
2264
2315
|
}
|
|
2265
2316
|
listMemberships(organizationId, options = {}) {
|
|
2266
|
-
const revoked = options.includeRevoked ? "" :
|
|
2317
|
+
const revoked = options.includeRevoked ? "" : `AND status = 'active'`;
|
|
2267
2318
|
return this.database.query(`SELECT * FROM organization_memberships WHERE organization_id = ? ${revoked} ORDER BY created_at, id`).all(organizationId).map(mapMembership);
|
|
2268
2319
|
}
|
|
2269
2320
|
updateMembership(input) {
|
|
@@ -2314,7 +2365,7 @@ class ControlPlaneStore {
|
|
|
2314
2365
|
return membership;
|
|
2315
2366
|
}
|
|
2316
2367
|
assertAnotherOwner(current) {
|
|
2317
|
-
const owners = Number(this.database.query(
|
|
2368
|
+
const owners = Number(this.database.query(`SELECT COUNT(*) AS count FROM organization_memberships WHERE organization_id = ? AND status = ? AND role_template = 'owner'`).get(current.organizationId, "active")?.count ?? 0);
|
|
2318
2369
|
if (owners <= 1)
|
|
2319
2370
|
throw new Error("Cannot remove or demote the last organization owner");
|
|
2320
2371
|
}
|
|
@@ -2873,7 +2924,7 @@ class ControlPlaneStore {
|
|
|
2873
2924
|
if (this.path === ":memory:")
|
|
2874
2925
|
throw new Error("Cannot create a filesystem backup for an in-memory control plane");
|
|
2875
2926
|
const backupPath = `${this.path}.${Date.now()}.bak`;
|
|
2876
|
-
|
|
2927
|
+
this.backupTo(backupPath);
|
|
2877
2928
|
this.setSetting("storage.last_backup", { path: backupPath, createdAt: this.now(), reason });
|
|
2878
2929
|
return backupPath;
|
|
2879
2930
|
}
|
|
@@ -7990,7 +8041,7 @@ function estimateExistingStaticFullStackMonthlyCost(options = {}) {
|
|
|
7990
8041
|
// src/deploy/container-image.ts
|
|
7991
8042
|
import { execFileSync } from "node:child_process";
|
|
7992
8043
|
import { createHash as createHash8 } from "node:crypto";
|
|
7993
|
-
import { existsSync as existsSync3, mkdirSync as mkdirSync2, mkdtempSync, readdirSync, readFileSync, rmSync, statSync as statSync2, writeFileSync
|
|
8044
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync2, mkdtempSync, readdirSync, readFileSync, rmSync, statSync as statSync2, writeFileSync } from "node:fs";
|
|
7994
8045
|
import { tmpdir } from "node:os";
|
|
7995
8046
|
import { basename as basename2, join as join3, relative, resolve as resolve2 } from "node:path";
|
|
7996
8047
|
var IGNORED_DIRECTORIES = new Set([".git", "node_modules", "dist", "coverage"]);
|
|
@@ -8093,7 +8144,7 @@ async function buildAndPushContainerImage(options, injected) {
|
|
|
8093
8144
|
const registry = auth.proxyEndpoint.replace(/^https?:\/\//, "");
|
|
8094
8145
|
const dockerConfig = join3(temporaryDirectory, "docker");
|
|
8095
8146
|
mkdirSync2(dockerConfig, { recursive: true });
|
|
8096
|
-
|
|
8147
|
+
writeFileSync(join3(dockerConfig, "config.json"), JSON.stringify({ auths: { [registry]: { auth: auth.authorizationToken } } }), { mode: 384 });
|
|
8097
8148
|
dependencies2.run("docker", ["push", imageUri], { env: { ...process.env, DOCKER_CONFIG: dockerConfig } });
|
|
8098
8149
|
} finally {
|
|
8099
8150
|
rmSync(temporaryDirectory, { recursive: true, force: true });
|
|
@@ -10895,9 +10946,11 @@ class TelemetryStore {
|
|
|
10895
10946
|
if (environmentId)
|
|
10896
10947
|
bindings.push(environmentId);
|
|
10897
10948
|
const boundedResources = [...new Set(resourceIds ?? [])].slice(0, 100);
|
|
10949
|
+
if (resourceIds && boundedResources.length === 0)
|
|
10950
|
+
return [];
|
|
10898
10951
|
const resources = resourceIds ? ` AND resource_id IN (${boundedResources.map(() => "?").join(",") || "''"})` : "";
|
|
10899
10952
|
bindings.push(...boundedResources);
|
|
10900
|
-
const rows = this.controlPlane.database.query(`SELECT source, MAX(
|
|
10953
|
+
const rows = this.controlPlane.database.query(`SELECT source, MAX(latest_observed_at) latest, SUM(ingested_bytes) bytes, SUM(record_count) count, MIN(first_observed_at) first FROM telemetry_source_rollups WHERE project_id=?${environment2}${resources} GROUP BY source`).all(...bindings);
|
|
10901
10954
|
const now = this.now().getTime();
|
|
10902
10955
|
return rows.map((row) => {
|
|
10903
10956
|
const latest = String(row.latest);
|
|
@@ -11016,8 +11069,26 @@ class TelemetryStore {
|
|
|
11016
11069
|
const excess = Number(this.controlPlane.database.query(`SELECT MAX(0, COUNT(*)-?) excess FROM telemetry_records${projectId ? " WHERE project_id=?" : ""}`).get(policy.maxRecords, ...projectId ? [projectId] : [])?.excess ?? 0);
|
|
11017
11070
|
if (excess > 0)
|
|
11018
11071
|
this.controlPlane.database.run(`DELETE FROM telemetry_records WHERE id IN (SELECT id FROM telemetry_records${projectId ? " WHERE project_id=?" : ""} ORDER BY timestamp ASC LIMIT ?)`, projectId ? [projectId, excess] : [excess]);
|
|
11072
|
+
this.rebuildStatusRollups(projectId);
|
|
11019
11073
|
return { deleted: removed + excess, ...downsampled };
|
|
11020
11074
|
}
|
|
11075
|
+
rebuildStatusRollups(projectId) {
|
|
11076
|
+
const scope = projectId ? " WHERE project_id=?" : "";
|
|
11077
|
+
const bindings = projectId ? [projectId] : [];
|
|
11078
|
+
const rebuild = this.controlPlane.database.transaction(() => {
|
|
11079
|
+
this.controlPlane.database.run(`DELETE FROM telemetry_source_rollups${scope}`, bindings);
|
|
11080
|
+
this.controlPlane.database.run(`INSERT INTO telemetry_source_rollups (
|
|
11081
|
+
project_id, environment_id, resource_id, source,
|
|
11082
|
+
first_observed_at, latest_observed_at, ingested_bytes, record_count
|
|
11083
|
+
)
|
|
11084
|
+
SELECT
|
|
11085
|
+
project_id, COALESCE(environment_id, ''), COALESCE(resource_id, ''), source,
|
|
11086
|
+
MIN(observed_at), MAX(observed_at), SUM(ingested_bytes), COUNT(*)
|
|
11087
|
+
FROM telemetry_records${scope}
|
|
11088
|
+
GROUP BY project_id, COALESCE(environment_id, ''), COALESCE(resource_id, ''), source`, bindings);
|
|
11089
|
+
});
|
|
11090
|
+
rebuild();
|
|
11091
|
+
}
|
|
11021
11092
|
}
|
|
11022
11093
|
// src/alerts/telemetry.ts
|
|
11023
11094
|
function evaluateTelemetryAlertRules(store4, projectId, environmentId, now = new Date) {
|
|
@@ -12075,7 +12146,7 @@ async function manifestRequest(fetchFn, root, image, token, signal, authorizatio
|
|
|
12075
12146
|
}
|
|
12076
12147
|
// src/onboarding/artifact-store.ts
|
|
12077
12148
|
import { createHash as createHash12 } from "node:crypto";
|
|
12078
|
-
import { chmodSync as chmodSync2, existsSync as existsSync5, mkdirSync as mkdirSync3, renameSync, unlinkSync, writeFileSync as
|
|
12149
|
+
import { chmodSync as chmodSync2, existsSync as existsSync5, mkdirSync as mkdirSync3, renameSync, unlinkSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
12079
12150
|
import { basename as basename3, join as join5 } from "node:path";
|
|
12080
12151
|
function map2(row) {
|
|
12081
12152
|
return {
|
|
@@ -12122,7 +12193,7 @@ class ApplicationArtifactStore {
|
|
|
12122
12193
|
const path = join5(this.root, `${id}.archive`);
|
|
12123
12194
|
const temporary = `${path}.partial`;
|
|
12124
12195
|
try {
|
|
12125
|
-
|
|
12196
|
+
writeFileSync2(temporary, input.bytes, { mode: 384, flag: "wx" });
|
|
12126
12197
|
renameSync(temporary, path);
|
|
12127
12198
|
chmodSync2(path, 384);
|
|
12128
12199
|
this.controlPlane.database.run("INSERT INTO application_artifacts (id, organization_id, project_id, filename, storage_path, sha256, size, format, entry_count, expanded_bytes, created_by_actor_id, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", [
|
|
@@ -14755,7 +14826,7 @@ function webhookEndpoint(baseUrl, webhook) {
|
|
|
14755
14826
|
return new URL(`/api/source/webhooks/${encodeURIComponent(webhook.endpointToken)}`, base).href;
|
|
14756
14827
|
}
|
|
14757
14828
|
// src/source/git-workspace.ts
|
|
14758
|
-
import { chmodSync as chmodSync3, mkdtempSync as mkdtempSync2, rmSync as rmSync2, writeFileSync as
|
|
14829
|
+
import { chmodSync as chmodSync3, mkdtempSync as mkdtempSync2, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "node:fs";
|
|
14759
14830
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
14760
14831
|
import { join as join6, resolve as resolve5 } from "node:path";
|
|
14761
14832
|
function validateRemote(value, deployKey) {
|
|
@@ -14802,8 +14873,8 @@ function transportEnvironment(options) {
|
|
|
14802
14873
|
directory = mkdtempSync2(join6(tmpdir2(), "ts-cloud-git-transport-"));
|
|
14803
14874
|
const keyPath = join6(directory, "key");
|
|
14804
14875
|
const knownHostsPath = join6(directory, "known_hosts");
|
|
14805
|
-
|
|
14806
|
-
|
|
14876
|
+
writeFileSync3(keyPath, options.deployKey.privateKey, { mode: 384 });
|
|
14877
|
+
writeFileSync3(knownHostsPath, `${options.deployKey.host} ${options.deployKey.hostKey}
|
|
14807
14878
|
`, { mode: 384 });
|
|
14808
14879
|
chmodSync3(keyPath, 384);
|
|
14809
14880
|
chmodSync3(knownHostsPath, 384);
|
|
@@ -18280,7 +18351,7 @@ async function sendAuthenticationEmail(config, message) {
|
|
|
18280
18351
|
}
|
|
18281
18352
|
// src/auth/encryption.ts
|
|
18282
18353
|
import { randomBytes as randomBytes6 } from "node:crypto";
|
|
18283
|
-
import { chmodSync as chmodSync4, existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync4, writeFileSync as
|
|
18354
|
+
import { chmodSync as chmodSync4, existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "node:fs";
|
|
18284
18355
|
import { dirname as dirname2 } from "node:path";
|
|
18285
18356
|
function authEncryptionKeyFile() {
|
|
18286
18357
|
return statePath("auth-encryption-key");
|
|
@@ -18300,7 +18371,7 @@ function resolveAuthEncryptionKey(cwd) {
|
|
|
18300
18371
|
const key = randomBytes6(32).toString("base64url");
|
|
18301
18372
|
try {
|
|
18302
18373
|
mkdirSync4(dirname2(file), { recursive: true, mode: 448 });
|
|
18303
|
-
|
|
18374
|
+
writeFileSync4(file, `${key}
|
|
18304
18375
|
`, { mode: 384 });
|
|
18305
18376
|
chmodSync4(file, 384);
|
|
18306
18377
|
} catch {}
|
|
@@ -23027,7 +23098,7 @@ import { homedir as homedir3 } from "os";
|
|
|
23027
23098
|
import { dirname as dirname4, resolve as resolve12 } from "path";
|
|
23028
23099
|
import process20 from "process";
|
|
23029
23100
|
import { existsSync as existsSync22, statSync as statSync22 } from "fs";
|
|
23030
|
-
import { existsSync as existsSync82, mkdirSync as mkdirSync32, readdirSync as readdirSync32, writeFileSync as
|
|
23101
|
+
import { existsSync as existsSync82, mkdirSync as mkdirSync32, readdirSync as readdirSync32, writeFileSync as writeFileSync5 } from "fs";
|
|
23031
23102
|
import { homedir as homedir2 } from "os";
|
|
23032
23103
|
import { dirname as dirname3, resolve as resolve72 } from "path";
|
|
23033
23104
|
import process12 from "process";
|
|
@@ -28162,7 +28233,7 @@ function generateConfigTypes(options) {
|
|
|
28162
28233
|
const content = `// Generated by bunfig v${version}
|
|
28163
28234
|
export type ConfigNames = ${files.length ? `'${files.join("' | '")}'` : "string"}
|
|
28164
28235
|
`;
|
|
28165
|
-
|
|
28236
|
+
writeFileSync5(outputFile, content, { mode: 438 });
|
|
28166
28237
|
}
|
|
28167
28238
|
function createLibraryConfig(options) {
|
|
28168
28239
|
let configCache = null;
|
|
@@ -39416,6 +39487,10 @@ async function resolveServerDashboardData(config6, environment2, options = {}) {
|
|
|
39416
39487
|
}
|
|
39417
39488
|
|
|
39418
39489
|
// src/deploy/dashboard-database.ts
|
|
39490
|
+
var EXTERNAL_ENGINES = new Set(["singlestore", "vitess"]);
|
|
39491
|
+
function isExternalEngine(engine) {
|
|
39492
|
+
return EXTERNAL_ENGINES.has(engine);
|
|
39493
|
+
}
|
|
39419
39494
|
var SOCKETS = {
|
|
39420
39495
|
mysql: "/var/lib/pantry/mysql/mysqld.sock",
|
|
39421
39496
|
mariadb: "/var/lib/pantry/mariadb/mariadbd.sock"
|
|
@@ -39428,6 +39503,10 @@ function normalizeEngine(engine) {
|
|
|
39428
39503
|
return "postgres";
|
|
39429
39504
|
if (engine === "mariadb")
|
|
39430
39505
|
return "mariadb";
|
|
39506
|
+
if (engine === "singlestore")
|
|
39507
|
+
return "singlestore";
|
|
39508
|
+
if (engine === "vitess")
|
|
39509
|
+
return "vitess";
|
|
39431
39510
|
return "mysql";
|
|
39432
39511
|
}
|
|
39433
39512
|
function resolveDbEngine(config6) {
|
|
@@ -39454,6 +39533,8 @@ function pgExec(sql, database) {
|
|
|
39454
39533
|
return [`${pgAdminCommand(database)} -tA <<'TS_CLOUD_PG_EOF'`, ...sql, "TS_CLOUD_PG_EOF"];
|
|
39455
39534
|
}
|
|
39456
39535
|
function buildListScript(engine, database) {
|
|
39536
|
+
if (engine === "vitess")
|
|
39537
|
+
return buildVitessListScript(database);
|
|
39457
39538
|
if (engine === "postgres") {
|
|
39458
39539
|
return pgExec([
|
|
39459
39540
|
"SELECT 'DB=' || datname FROM pg_database WHERE datistemplate = false;",
|
|
@@ -39465,6 +39546,72 @@ function buildListScript(engine, database) {
|
|
|
39465
39546
|
"SELECT DISTINCT CONCAT('USER=', User) FROM mysql.user WHERE User NOT IN ('root', 'mysql.sys', 'mysql.session', 'mysql.infoschema', 'debian-sys-maint');"
|
|
39466
39547
|
]);
|
|
39467
39548
|
}
|
|
39549
|
+
function externalMysqlExec(sql, database, port) {
|
|
39550
|
+
const host2 = database?.host ?? "127.0.0.1";
|
|
39551
|
+
const user = database?.username ?? "root";
|
|
39552
|
+
const pass = database?.password ?? "";
|
|
39553
|
+
const tls = database?.ssl === false ? "" : " --ssl-mode=PREFERRED";
|
|
39554
|
+
const shq = (v) => `'${String(v).replace(/'/g, `'\\''`)}'`;
|
|
39555
|
+
const env = pass ? `MYSQL_PWD=${shq(pass)} ` : "";
|
|
39556
|
+
return [
|
|
39557
|
+
`${env}mysql -h ${shq(host2)} -P ${database?.port ?? port} -u ${shq(user)}${tls} --batch --skip-column-names <<'TS_CLOUD_SQL_EOF'`,
|
|
39558
|
+
...sql,
|
|
39559
|
+
"TS_CLOUD_SQL_EOF"
|
|
39560
|
+
];
|
|
39561
|
+
}
|
|
39562
|
+
var VTGATE_DEFAULT_PORT = 15306;
|
|
39563
|
+
function buildVitessListScript(database) {
|
|
39564
|
+
const script = externalMysqlExec(["SHOW KEYSPACES;"], database, VTGATE_DEFAULT_PORT);
|
|
39565
|
+
script[0] = `${script[0]} | sed 's/^/DB=/'`;
|
|
39566
|
+
return script;
|
|
39567
|
+
}
|
|
39568
|
+
function buildVitessTopologyScript(database) {
|
|
39569
|
+
return externalMysqlExec([
|
|
39570
|
+
`SHOW KEYSPACES;`,
|
|
39571
|
+
`SHOW VITESS_SHARDS;`,
|
|
39572
|
+
`SHOW VITESS_TABLETS;`
|
|
39573
|
+
], database, VTGATE_DEFAULT_PORT);
|
|
39574
|
+
}
|
|
39575
|
+
function parseVitessTopology(output) {
|
|
39576
|
+
const keyspaces = [];
|
|
39577
|
+
const shards = [];
|
|
39578
|
+
const tablets = [];
|
|
39579
|
+
for (const rawLine of output.split(`
|
|
39580
|
+
`)) {
|
|
39581
|
+
const line = rawLine.trim();
|
|
39582
|
+
if (!line)
|
|
39583
|
+
continue;
|
|
39584
|
+
const cols = line.split("\t").map((c) => c.trim()).filter(Boolean);
|
|
39585
|
+
if (cols.length >= 6) {
|
|
39586
|
+
const [cell, keyspace, shard, type, state, alias, hostname] = cols;
|
|
39587
|
+
tablets.push({
|
|
39588
|
+
cell: cell ?? "",
|
|
39589
|
+
keyspace: keyspace ?? "",
|
|
39590
|
+
shard: shard ?? "",
|
|
39591
|
+
type: type ?? "",
|
|
39592
|
+
state: state ?? "",
|
|
39593
|
+
alias: alias ?? "",
|
|
39594
|
+
hostname: hostname ?? ""
|
|
39595
|
+
});
|
|
39596
|
+
continue;
|
|
39597
|
+
}
|
|
39598
|
+
if (cols.length === 1) {
|
|
39599
|
+
const only = cols[0];
|
|
39600
|
+
if (only.includes("/")) {
|
|
39601
|
+
const [keyspace, shard] = only.split("/");
|
|
39602
|
+
if (keyspace && shard)
|
|
39603
|
+
shards.push({ keyspace, shard });
|
|
39604
|
+
} else {
|
|
39605
|
+
keyspaces.push(only);
|
|
39606
|
+
}
|
|
39607
|
+
}
|
|
39608
|
+
}
|
|
39609
|
+
return {
|
|
39610
|
+
keyspaces: [...new Set(keyspaces)].filter(Boolean).sort(),
|
|
39611
|
+
shards,
|
|
39612
|
+
tablets
|
|
39613
|
+
};
|
|
39614
|
+
}
|
|
39468
39615
|
function buildCreateDatabaseScript(engine, name, database) {
|
|
39469
39616
|
if (engine === "postgres") {
|
|
39470
39617
|
return pgExec([
|
|
@@ -39541,18 +39688,31 @@ async function runDb(config6, environment2, commands, comment) {
|
|
|
39541
39688
|
stderr: result2.perInstance?.[0]?.error ?? result2.error ?? ""
|
|
39542
39689
|
};
|
|
39543
39690
|
}
|
|
39691
|
+
function externalEngineRefusal(engine, action) {
|
|
39692
|
+
const where = engine === "vitess" ? "Create a keyspace with `vtctldclient CreateKeyspace`, and manage vtgate credentials on the cluster." : "Use the provider console or the cluster's own admin connection.";
|
|
39693
|
+
return {
|
|
39694
|
+
ok: false,
|
|
39695
|
+
error: `${action} is not available for ${engine}: it is an external cluster, so this box has no engine to administer. ${where}`
|
|
39696
|
+
};
|
|
39697
|
+
}
|
|
39544
39698
|
async function listDatabases(config6, environment2) {
|
|
39545
39699
|
const engine = resolveDbEngine(config6);
|
|
39700
|
+
if (isExternalEngine(engine) && engine !== "vitess")
|
|
39701
|
+
return { ...externalEngineRefusal(engine, "Listing databases"), engine, databases: [], users: [] };
|
|
39546
39702
|
const r = await runDb(config6, environment2, buildListScript(engine, resolveAppDatabase(config6)), "ts-cloud db:list");
|
|
39547
39703
|
const parsed = r.ok && r.stdout ? parseDbList(r.stdout) : { databases: [], users: [] };
|
|
39548
39704
|
return { ...r, engine, ...parsed };
|
|
39549
39705
|
}
|
|
39550
39706
|
async function createDatabase(config6, environment2, name) {
|
|
39551
39707
|
const engine = resolveDbEngine(config6);
|
|
39708
|
+
if (isExternalEngine(engine))
|
|
39709
|
+
return externalEngineRefusal(engine, "Creating a database");
|
|
39552
39710
|
return runDb(config6, environment2, buildCreateDatabaseScript(engine, name, resolveAppDatabase(config6)), `ts-cloud db:create ${name}`);
|
|
39553
39711
|
}
|
|
39554
39712
|
async function createDatabaseUser(config6, environment2, input) {
|
|
39555
39713
|
const engine = resolveDbEngine(config6);
|
|
39714
|
+
if (isExternalEngine(engine))
|
|
39715
|
+
return externalEngineRefusal(engine, "Creating a database user");
|
|
39556
39716
|
return runDb(config6, environment2, buildCreateUserScript(engine, input, resolveAppDatabase(config6)), `ts-cloud db:user ${input.username}`);
|
|
39557
39717
|
}
|
|
39558
39718
|
var DB_BACKUP_DIR = "/var/backups/ts-cloud/databases";
|
|
@@ -39596,6 +39756,8 @@ async function backupDatabase(config6, environment2, name) {
|
|
|
39596
39756
|
if (!isValidDbIdentifier(name))
|
|
39597
39757
|
return { ok: false, error: "Database name must be a valid identifier.", database: name };
|
|
39598
39758
|
const engine = resolveDbEngine(config6);
|
|
39759
|
+
if (isExternalEngine(engine))
|
|
39760
|
+
return { ...externalEngineRefusal(engine, "Backing up"), database: name };
|
|
39599
39761
|
const r = await runDb(config6, environment2, buildBackupScript(engine, name, DB_BACKUP_DIR, resolveAppDatabase(config6)), `ts-cloud db:backup ${name}`);
|
|
39600
39762
|
return { ...r, database: name };
|
|
39601
39763
|
}
|
|
@@ -39604,6 +39766,233 @@ async function listDatabaseBackups(config6, environment2) {
|
|
|
39604
39766
|
const backups = r.ok && r.stdout ? parseBackups(r.stdout) : [];
|
|
39605
39767
|
return { ...r, backups };
|
|
39606
39768
|
}
|
|
39769
|
+
async function describeVitess(config6, environment2) {
|
|
39770
|
+
const engine = resolveDbEngine(config6);
|
|
39771
|
+
const empty = { keyspaces: [], shards: [], tablets: [] };
|
|
39772
|
+
if (engine !== "vitess")
|
|
39773
|
+
return { ok: false, error: "The configured database engine is not Vitess.", engine, ...empty };
|
|
39774
|
+
const r = await runDb(config6, environment2, buildVitessTopologyScript(resolveAppDatabase(config6)), "ts-cloud vitess:topology");
|
|
39775
|
+
const parsed = r.ok && r.stdout ? parseVitessTopology(r.stdout) : empty;
|
|
39776
|
+
return { ...r, engine, ...parsed };
|
|
39777
|
+
}
|
|
39778
|
+
function unhealthyTablets(tablets) {
|
|
39779
|
+
return tablets.filter((t) => t.state.toUpperCase() !== "SERVING");
|
|
39780
|
+
}
|
|
39781
|
+
function shardsMissingPrimary(topology2) {
|
|
39782
|
+
const withPrimary = new Set(topology2.tablets.filter((t) => t.type.toUpperCase() === "PRIMARY" && t.state.toUpperCase() === "SERVING").map((t) => `${t.keyspace}/${t.shard}`));
|
|
39783
|
+
return topology2.shards.filter((s) => !withPrimary.has(`${s.keyspace}/${s.shard}`));
|
|
39784
|
+
}
|
|
39785
|
+
|
|
39786
|
+
// src/deploy/dashboard-vitess.ts
|
|
39787
|
+
var VTCTLDCLIENT_BIN = "/usr/local/bin/vtctldclient";
|
|
39788
|
+
var DEFAULT_VTCTLDCLIENT_VERSION = "21.0.0";
|
|
39789
|
+
function sh(value2) {
|
|
39790
|
+
return `'${String(value2).split("'").join("'\\''")}'`;
|
|
39791
|
+
}
|
|
39792
|
+
function resolveVitessConfig(config6) {
|
|
39793
|
+
return resolveAppDatabase(config6)?.vitess ?? {};
|
|
39794
|
+
}
|
|
39795
|
+
function isValidKeyspaceName(value2) {
|
|
39796
|
+
return /^[A-Za-z_][A-Za-z0-9_]{0,63}$/.test(value2);
|
|
39797
|
+
}
|
|
39798
|
+
function isValidMigrationUuid(value2) {
|
|
39799
|
+
return /^[0-9a-f]{8}(?:_[0-9a-f]{4}){3}_[0-9a-f]{12}$/i.test(value2);
|
|
39800
|
+
}
|
|
39801
|
+
function buildVtctldClientInstallScript(version3 = DEFAULT_VTCTLDCLIENT_VERSION) {
|
|
39802
|
+
const v = sh(version3);
|
|
39803
|
+
return [
|
|
39804
|
+
`if ${VTCTLDCLIENT_BIN} --version 2>/dev/null | grep -q ${v}; then echo "vtctldclient ${version3} already installed"; else`,
|
|
39805
|
+
" command -v curl >/dev/null 2>&1 || (apt-get update -y && apt-get install -y curl ca-certificates)",
|
|
39806
|
+
' if [ "$(uname -m)" != "x86_64" ]; then',
|
|
39807
|
+
' echo "vtctldclient: Vitess publishes only an x86_64 release tarball; this box is $(uname -m). Build from source or install via pantry." >&2',
|
|
39808
|
+
" exit 1",
|
|
39809
|
+
" fi",
|
|
39810
|
+
' tmp="$(mktemp -d)"',
|
|
39811
|
+
` api="https://api.github.com/repos/vitessio/vitess/releases/tags/v${version3}"`,
|
|
39812
|
+
` url="$(curl -fsSL "$api" | grep -oE '"browser_download_url"[[:space:]]*:[[:space:]]*"[^"]+vitess-[0-9][^"]*\\.tar\\.gz"' | head -1 | sed -E 's/.*"(https[^"]+)"/\\1/')"`,
|
|
39813
|
+
' if [ -z "$url" ]; then echo "vtctldclient: no release tarball found for v' + version3 + '" >&2; rm -rf "$tmp"; exit 1; fi',
|
|
39814
|
+
` curl -fsSL "$url" | tar -xz -C "$tmp" --strip-components=2 --wildcards --occurrence=1 '*/bin/vtctldclient' || true`,
|
|
39815
|
+
' if [ ! -s "$tmp/vtctldclient" ]; then',
|
|
39816
|
+
' echo "vtctldclient: could not extract the binary from $url" >&2',
|
|
39817
|
+
' rm -rf "$tmp"; exit 1',
|
|
39818
|
+
" fi",
|
|
39819
|
+
` install -m 0755 "$tmp/vtctldclient" ${VTCTLDCLIENT_BIN}`,
|
|
39820
|
+
' rm -rf "$tmp"',
|
|
39821
|
+
` echo "installed vtctldclient ${version3}"`,
|
|
39822
|
+
"fi"
|
|
39823
|
+
];
|
|
39824
|
+
}
|
|
39825
|
+
function vtctldCommand(vitess) {
|
|
39826
|
+
if (!vitess.vtctldAddr)
|
|
39827
|
+
return null;
|
|
39828
|
+
return `${VTCTLDCLIENT_BIN} --server ${sh(vitess.vtctldAddr)}`;
|
|
39829
|
+
}
|
|
39830
|
+
function buildCreateKeyspaceScript(vitess, name, options = {}) {
|
|
39831
|
+
const base = vtctldCommand(vitess);
|
|
39832
|
+
if (!base)
|
|
39833
|
+
return null;
|
|
39834
|
+
const sharded = options.sharded ? " --sharded" : "";
|
|
39835
|
+
return [`${base} CreateKeyspace${sharded} ${sh(name)}`];
|
|
39836
|
+
}
|
|
39837
|
+
function buildApplyVSchemaScript(vitess, keyspace, vschemaJson) {
|
|
39838
|
+
const base = vtctldCommand(vitess);
|
|
39839
|
+
if (!base)
|
|
39840
|
+
return null;
|
|
39841
|
+
return [
|
|
39842
|
+
'vschema_tmp="$(mktemp)"',
|
|
39843
|
+
`cat > "$vschema_tmp" <<'TS_CLOUD_VSCHEMA_EOF'`,
|
|
39844
|
+
vschemaJson,
|
|
39845
|
+
"TS_CLOUD_VSCHEMA_EOF",
|
|
39846
|
+
`${base} ApplyVSchema --vschema-file "$vschema_tmp" ${sh(keyspace)}`,
|
|
39847
|
+
'rm -f "$vschema_tmp"'
|
|
39848
|
+
];
|
|
39849
|
+
}
|
|
39850
|
+
function buildApplySchemaScript(database, sql, strategy2 = "vitess") {
|
|
39851
|
+
return externalMysqlExec([`SET @@ddl_strategy='${strategy2}';`, sql.endsWith(";") ? sql : `${sql};`], database, VTGATE_DEFAULT_PORT);
|
|
39852
|
+
}
|
|
39853
|
+
function buildMigrationsScript(database, keyspace) {
|
|
39854
|
+
const statements = keyspace ? [`USE ${keyspace.replace(/[^A-Z0-9_]/gi, "")};`, "SHOW VITESS_MIGRATIONS;"] : ["SHOW VITESS_MIGRATIONS;"];
|
|
39855
|
+
const script = externalMysqlExec(statements, database, VTGATE_DEFAULT_PORT);
|
|
39856
|
+
script[0] = script[0].replace(" --skip-column-names", "");
|
|
39857
|
+
return script;
|
|
39858
|
+
}
|
|
39859
|
+
function parseMigrations(output) {
|
|
39860
|
+
const lines = output.split(`
|
|
39861
|
+
`).map((l) => l.trimEnd()).filter((l) => l.trim().length > 0);
|
|
39862
|
+
if (lines.length === 0)
|
|
39863
|
+
return [];
|
|
39864
|
+
const header3 = lines[0].split("\t").map((c) => c.trim());
|
|
39865
|
+
const col = (name) => header3.indexOf(name);
|
|
39866
|
+
const columns = {
|
|
39867
|
+
uuid: col("migration_uuid"),
|
|
39868
|
+
keyspace: col("keyspace"),
|
|
39869
|
+
shard: col("shard"),
|
|
39870
|
+
table: col("mysql_table"),
|
|
39871
|
+
status: col("migration_status"),
|
|
39872
|
+
strategy: col("strategy"),
|
|
39873
|
+
added: col("added_timestamp"),
|
|
39874
|
+
completed: col("completed_timestamp"),
|
|
39875
|
+
progress: col("progress")
|
|
39876
|
+
};
|
|
39877
|
+
if (columns.uuid === -1)
|
|
39878
|
+
return [];
|
|
39879
|
+
const rows = [];
|
|
39880
|
+
for (const line of lines.slice(1)) {
|
|
39881
|
+
const cells = line.split("\t");
|
|
39882
|
+
const at = (i) => i >= 0 ? (cells[i] ?? "").trim() : "";
|
|
39883
|
+
const uuid = at(columns.uuid);
|
|
39884
|
+
if (!uuid)
|
|
39885
|
+
continue;
|
|
39886
|
+
rows.push({
|
|
39887
|
+
uuid,
|
|
39888
|
+
keyspace: at(columns.keyspace),
|
|
39889
|
+
shard: at(columns.shard),
|
|
39890
|
+
table: at(columns.table),
|
|
39891
|
+
status: at(columns.status),
|
|
39892
|
+
strategy: at(columns.strategy),
|
|
39893
|
+
added: at(columns.added),
|
|
39894
|
+
completed: at(columns.completed),
|
|
39895
|
+
progress: at(columns.progress)
|
|
39896
|
+
});
|
|
39897
|
+
}
|
|
39898
|
+
return rows;
|
|
39899
|
+
}
|
|
39900
|
+
function failedMigrations(migrations2) {
|
|
39901
|
+
return migrations2.filter((m) => m.status.toLowerCase() === "failed");
|
|
39902
|
+
}
|
|
39903
|
+
function runningMigrations(migrations2) {
|
|
39904
|
+
return migrations2.filter((m) => ["running", "queued", "ready"].includes(m.status.toLowerCase()));
|
|
39905
|
+
}
|
|
39906
|
+
function buildMigrationActionScript(database, uuid, action) {
|
|
39907
|
+
const verb = action.toUpperCase();
|
|
39908
|
+
return externalMysqlExec([`ALTER VITESS_MIGRATION '${uuid}' ${verb};`], database, VTGATE_DEFAULT_PORT);
|
|
39909
|
+
}
|
|
39910
|
+
function requireVitess(config6) {
|
|
39911
|
+
if (resolveDbEngine(config6) !== "vitess")
|
|
39912
|
+
return { ok: false, error: "The configured database engine is not Vitess." };
|
|
39913
|
+
return null;
|
|
39914
|
+
}
|
|
39915
|
+
function requireControlPlane(vitess) {
|
|
39916
|
+
if (!vitess.vtctldAddr) {
|
|
39917
|
+
return {
|
|
39918
|
+
ok: false,
|
|
39919
|
+
error: "No vtctld address is configured. Creating keyspaces and applying a VSchema are topology operations that vtgate cannot perform. " + "Set `infrastructure.appDatabase.vitess.vtctldAddr` (for example `vtctld.internal:15999`) and redeploy."
|
|
39920
|
+
};
|
|
39921
|
+
}
|
|
39922
|
+
return null;
|
|
39923
|
+
}
|
|
39924
|
+
async function installVtctldClient(config6, environment2) {
|
|
39925
|
+
const wrong = requireVitess(config6);
|
|
39926
|
+
if (wrong)
|
|
39927
|
+
return wrong;
|
|
39928
|
+
const vitess = resolveVitessConfig(config6);
|
|
39929
|
+
return runDb(config6, environment2, buildVtctldClientInstallScript(vitess.clientVersion), "ts-cloud vitess:install-client");
|
|
39930
|
+
}
|
|
39931
|
+
async function createKeyspace(config6, environment2, name, options = {}) {
|
|
39932
|
+
const wrong = requireVitess(config6);
|
|
39933
|
+
if (wrong)
|
|
39934
|
+
return { ...wrong, keyspace: name };
|
|
39935
|
+
if (!isValidKeyspaceName(name))
|
|
39936
|
+
return { ok: false, error: "Keyspace name must be a valid identifier.", keyspace: name };
|
|
39937
|
+
const vitess = resolveVitessConfig(config6);
|
|
39938
|
+
const missing = requireControlPlane(vitess);
|
|
39939
|
+
if (missing)
|
|
39940
|
+
return { ...missing, keyspace: name };
|
|
39941
|
+
const script = buildCreateKeyspaceScript(vitess, name, options);
|
|
39942
|
+
if (!script)
|
|
39943
|
+
return { ...requireControlPlane({}), keyspace: name };
|
|
39944
|
+
const r = await runDb(config6, environment2, script, `ts-cloud vitess:create-keyspace ${name}`);
|
|
39945
|
+
return { ...r, keyspace: name };
|
|
39946
|
+
}
|
|
39947
|
+
async function applyVSchema(config6, environment2, keyspace, vschemaJson) {
|
|
39948
|
+
const wrong = requireVitess(config6);
|
|
39949
|
+
if (wrong)
|
|
39950
|
+
return { ...wrong, keyspace };
|
|
39951
|
+
if (!isValidKeyspaceName(keyspace))
|
|
39952
|
+
return { ok: false, error: "Keyspace name must be a valid identifier.", keyspace };
|
|
39953
|
+
try {
|
|
39954
|
+
const parsed = JSON.parse(vschemaJson);
|
|
39955
|
+
if (!parsed || typeof parsed !== "object")
|
|
39956
|
+
return { ok: false, error: "The VSchema must be a JSON object.", keyspace };
|
|
39957
|
+
} catch (error2) {
|
|
39958
|
+
return { ok: false, error: `The VSchema is not valid JSON: ${error2?.message ?? error2}`, keyspace };
|
|
39959
|
+
}
|
|
39960
|
+
const vitess = resolveVitessConfig(config6);
|
|
39961
|
+
const missing = requireControlPlane(vitess);
|
|
39962
|
+
if (missing)
|
|
39963
|
+
return { ...missing, keyspace };
|
|
39964
|
+
const script = buildApplyVSchemaScript(vitess, keyspace, vschemaJson);
|
|
39965
|
+
if (!script)
|
|
39966
|
+
return { ...requireControlPlane({}), keyspace };
|
|
39967
|
+
const r = await runDb(config6, environment2, script, `ts-cloud vitess:apply-vschema ${keyspace}`);
|
|
39968
|
+
return { ...r, keyspace };
|
|
39969
|
+
}
|
|
39970
|
+
async function applySchemaChange(config6, environment2, sql) {
|
|
39971
|
+
const wrong = requireVitess(config6);
|
|
39972
|
+
if (wrong)
|
|
39973
|
+
return wrong;
|
|
39974
|
+
if (!sql.trim())
|
|
39975
|
+
return { ok: false, error: "No SQL was provided." };
|
|
39976
|
+
const vitess = resolveVitessConfig(config6);
|
|
39977
|
+
return runDb(config6, environment2, buildApplySchemaScript(resolveAppDatabase(config6), sql, vitess.ddlStrategy ?? "vitess"), "ts-cloud vitess:apply-schema");
|
|
39978
|
+
}
|
|
39979
|
+
async function listMigrations(config6, environment2, keyspace) {
|
|
39980
|
+
const wrong = requireVitess(config6);
|
|
39981
|
+
if (wrong)
|
|
39982
|
+
return { ...wrong, migrations: [], failed: [], running: [] };
|
|
39983
|
+
const r = await runDb(config6, environment2, buildMigrationsScript(resolveAppDatabase(config6), keyspace), "ts-cloud vitess:migrations");
|
|
39984
|
+
const migrations2 = r.ok && r.stdout ? parseMigrations(r.stdout) : [];
|
|
39985
|
+
return { ...r, migrations: migrations2, failed: failedMigrations(migrations2), running: runningMigrations(migrations2) };
|
|
39986
|
+
}
|
|
39987
|
+
async function actOnMigration(config6, environment2, uuid, action) {
|
|
39988
|
+
const wrong = requireVitess(config6);
|
|
39989
|
+
if (wrong)
|
|
39990
|
+
return { ...wrong, uuid };
|
|
39991
|
+
if (!isValidMigrationUuid(uuid))
|
|
39992
|
+
return { ok: false, error: "Migration UUID is not in the expected format.", uuid };
|
|
39993
|
+
const r = await runDb(config6, environment2, buildMigrationActionScript(resolveAppDatabase(config6), uuid, action), `ts-cloud vitess:migration ${action} ${uuid}`);
|
|
39994
|
+
return { ...r, uuid };
|
|
39995
|
+
}
|
|
39607
39996
|
|
|
39608
39997
|
// src/deploy/dashboard-policy.ts
|
|
39609
39998
|
var FAIL_CLOSED = {
|
|
@@ -39843,6 +40232,13 @@ var POLICIES = {
|
|
|
39843
40232
|
"DELETE /api/firewall": { capability: "fleet:manage" },
|
|
39844
40233
|
"GET /api/databases": { capability: "data:read" },
|
|
39845
40234
|
"POST /api/databases": { capability: "data:admin" },
|
|
40235
|
+
"GET /api/databases/vitess": { capability: "data:read" },
|
|
40236
|
+
"GET /api/databases/vitess/migrations": { capability: "data:read" },
|
|
40237
|
+
"POST /api/databases/vitess/migrations/action": { capability: "data:admin" },
|
|
40238
|
+
"POST /api/databases/vitess/keyspaces": { capability: "data:admin" },
|
|
40239
|
+
"POST /api/databases/vitess/vschema": { capability: "data:admin" },
|
|
40240
|
+
"POST /api/databases/vitess/schema": { capability: "data:admin" },
|
|
40241
|
+
"POST /api/databases/vitess/install-client": { capability: "data:admin" },
|
|
39846
40242
|
"GET /api/databases/backups": { capability: "backups:read" },
|
|
39847
40243
|
"POST /api/databases/backup": { capability: "backups:create" },
|
|
39848
40244
|
"POST /api/databases/users": { capability: "data:admin" },
|
|
@@ -40133,7 +40529,7 @@ function clearSessionCookie(options = { secure: true }) {
|
|
|
40133
40529
|
}
|
|
40134
40530
|
|
|
40135
40531
|
// src/deploy/dashboard-users.ts
|
|
40136
|
-
import { chmodSync as chmodSync6, existsSync as existsSync19, mkdirSync as mkdirSync7, readFileSync as readFileSync10, writeFileSync as
|
|
40532
|
+
import { chmodSync as chmodSync6, existsSync as existsSync19, mkdirSync as mkdirSync7, readFileSync as readFileSync10, writeFileSync as writeFileSync9 } from "node:fs";
|
|
40137
40533
|
import { dirname as dirname8 } from "node:path";
|
|
40138
40534
|
function usersFile() {
|
|
40139
40535
|
return statePath("dashboard-users.json");
|
|
@@ -40188,7 +40584,7 @@ function loadUsers(cwd) {
|
|
|
40188
40584
|
function saveUsers(cwd, users) {
|
|
40189
40585
|
const file = usersFilePath(cwd);
|
|
40190
40586
|
mkdirSync7(dirname8(file), { recursive: true });
|
|
40191
|
-
|
|
40587
|
+
writeFileSync9(file, `${JSON.stringify({ users }, null, 2)}
|
|
40192
40588
|
`);
|
|
40193
40589
|
chmodSync6(file, 384);
|
|
40194
40590
|
}
|
|
@@ -48816,6 +49212,45 @@ data: ${JSON.stringify({ cursor: position, at: new Date().toISOString() })}
|
|
|
48816
49212
|
}, 422);
|
|
48817
49213
|
return json15({ ...await createDatabase(config6, environment2, name), name });
|
|
48818
49214
|
}
|
|
49215
|
+
if (url.pathname === "/api/databases/vitess" && req.method === "GET") {
|
|
49216
|
+
const topology2 = await describeVitess(config6, environment2);
|
|
49217
|
+
return json15({
|
|
49218
|
+
...topology2,
|
|
49219
|
+
unhealthy: unhealthyTablets(topology2.tablets),
|
|
49220
|
+
missingPrimary: shardsMissingPrimary(topology2)
|
|
49221
|
+
});
|
|
49222
|
+
}
|
|
49223
|
+
if (url.pathname === "/api/databases/vitess/migrations" && req.method === "GET") {
|
|
49224
|
+
const keyspace = url.searchParams.get("keyspace") ?? undefined;
|
|
49225
|
+
return json15(await listMigrations(config6, environment2, keyspace));
|
|
49226
|
+
}
|
|
49227
|
+
if (url.pathname === "/api/databases/vitess/migrations/action" && req.method === "POST") {
|
|
49228
|
+
const body = await readJsonBody(req);
|
|
49229
|
+
const uuid = String(body.uuid ?? "").trim();
|
|
49230
|
+
const action = String(body.action ?? "");
|
|
49231
|
+
if (!["retry", "cancel", "cleanup", "complete"].includes(action))
|
|
49232
|
+
return json15({ ok: false, error: "Unknown migration action." }, 422);
|
|
49233
|
+
return json15(await actOnMigration(config6, environment2, uuid, action));
|
|
49234
|
+
}
|
|
49235
|
+
if (url.pathname === "/api/databases/vitess/keyspaces" && req.method === "POST") {
|
|
49236
|
+
const body = await readJsonBody(req);
|
|
49237
|
+
const name = String(body.name ?? "").trim();
|
|
49238
|
+
const sharded = body.sharded === true;
|
|
49239
|
+
return json15(await createKeyspace(config6, environment2, name, { sharded }));
|
|
49240
|
+
}
|
|
49241
|
+
if (url.pathname === "/api/databases/vitess/vschema" && req.method === "POST") {
|
|
49242
|
+
const body = await readJsonBody(req);
|
|
49243
|
+
const keyspace = String(body.keyspace ?? "").trim();
|
|
49244
|
+
const vschema = typeof body.vschema === "string" ? body.vschema : JSON.stringify(body.vschema ?? null);
|
|
49245
|
+
return json15(await applyVSchema(config6, environment2, keyspace, vschema));
|
|
49246
|
+
}
|
|
49247
|
+
if (url.pathname === "/api/databases/vitess/schema" && req.method === "POST") {
|
|
49248
|
+
const body = await readJsonBody(req);
|
|
49249
|
+
const sql = String(body.sql ?? "");
|
|
49250
|
+
return json15(await applySchemaChange(config6, environment2, sql));
|
|
49251
|
+
}
|
|
49252
|
+
if (url.pathname === "/api/databases/vitess/install-client" && req.method === "POST")
|
|
49253
|
+
return json15(await installVtctldClient(config6, environment2));
|
|
48819
49254
|
if (url.pathname === "/api/databases/backups" && req.method === "GET")
|
|
48820
49255
|
return json15(await listDatabaseBackups(config6, environment2));
|
|
48821
49256
|
if (url.pathname === "/api/databases/backup" && req.method === "POST") {
|