@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
|
@@ -127,6 +127,8 @@ var PANTRY_PACKAGES = {
|
|
|
127
127
|
mysql: "mysql.com",
|
|
128
128
|
mariadb: "mariadb.com/server",
|
|
129
129
|
postgres: "postgresql.org",
|
|
130
|
+
vitess: "vitess.io",
|
|
131
|
+
etcd: "etcd.io",
|
|
130
132
|
redis: "redis.io",
|
|
131
133
|
memcached: "memcached.org",
|
|
132
134
|
meilisearch: "meilisearch.com",
|
|
@@ -171,8 +173,284 @@ function buildPantryServiceScript(services) {
|
|
|
171
173
|
]);
|
|
172
174
|
}
|
|
173
175
|
|
|
176
|
+
// src/drivers/shared/vitess-provision.ts
|
|
177
|
+
var PANTRY_BIN = `${PANTRY_PROJECT_DIR}/pantry/.bin`;
|
|
178
|
+
var VTGATE_MYSQL_PORT = 15306;
|
|
179
|
+
var VTGATE_GRPC_PORT = 15991;
|
|
180
|
+
var VTCTLD_GRPC_PORT = 15999;
|
|
181
|
+
var VTTABLET_GRPC_PORT = 16101;
|
|
182
|
+
var ETCD_CLIENT_PORT = 2379;
|
|
183
|
+
var VITESS_ROOT = "/var/lib/vitess";
|
|
184
|
+
var VITESS_TABLET_UID = 100;
|
|
185
|
+
function enabled(value) {
|
|
186
|
+
return value === true || typeof value === "object" && value !== null;
|
|
187
|
+
}
|
|
188
|
+
function settings(value) {
|
|
189
|
+
return typeof value === "object" && value !== null ? value : {};
|
|
190
|
+
}
|
|
191
|
+
function sh2(value) {
|
|
192
|
+
return `'${String(value).split("'").join("'\\''")}'`;
|
|
193
|
+
}
|
|
194
|
+
function vitessPackages(config) {
|
|
195
|
+
const version = config.version ? `vitess.io@${config.version}` : "vitess.io";
|
|
196
|
+
const packages = [version];
|
|
197
|
+
if (config.mode !== "combo" && !config.etcdEndpoint)
|
|
198
|
+
packages.push("etcd.io");
|
|
199
|
+
packages.push("mysql.com");
|
|
200
|
+
return packages;
|
|
201
|
+
}
|
|
202
|
+
var VITESS_USER = "vitess";
|
|
203
|
+
var UNIT_PATH = `${PANTRY_BIN}:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin`;
|
|
204
|
+
var VITESS_LIB = "/usr/local/lib/vitess";
|
|
205
|
+
var launchers = new Map;
|
|
206
|
+
function launcherPath(name) {
|
|
207
|
+
return `${VITESS_LIB}/${name}.sh`;
|
|
208
|
+
}
|
|
209
|
+
function launcherFor(name, command) {
|
|
210
|
+
launchers.set(name, launcherScript(command));
|
|
211
|
+
return `/bin/sh ${launcherPath(name)}`;
|
|
212
|
+
}
|
|
213
|
+
function buildLauncherScripts() {
|
|
214
|
+
const out = [`mkdir -p ${VITESS_LIB}`];
|
|
215
|
+
for (const [name, body] of launchers) {
|
|
216
|
+
out.push(`cat > ${launcherPath(name)} <<'TS_CLOUD_VITESS_LAUNCHER_EOF'`, body, "TS_CLOUD_VITESS_LAUNCHER_EOF", `chmod 0755 ${launcherPath(name)}`);
|
|
217
|
+
}
|
|
218
|
+
return out;
|
|
219
|
+
}
|
|
220
|
+
function launcherScript(command) {
|
|
221
|
+
return [
|
|
222
|
+
"#!/bin/sh",
|
|
223
|
+
"set -e",
|
|
224
|
+
`cd ${PANTRY_PROJECT_DIR}`,
|
|
225
|
+
'eval "$(pantry env 2>/dev/null)" || true',
|
|
226
|
+
`VT_MYSQL_ROOT="$(dirname "$(dirname "$(readlink -f ${PANTRY_BIN}/mysqld 2>/dev/null)")")"`,
|
|
227
|
+
`VTROOT="$(dirname "$(dirname "$(readlink -f ${PANTRY_BIN}/mysqlctld 2>/dev/null)")")"`,
|
|
228
|
+
"export VT_MYSQL_ROOT VTROOT",
|
|
229
|
+
`exec ${command.join(" ")}`
|
|
230
|
+
].join(`
|
|
231
|
+
`);
|
|
232
|
+
}
|
|
233
|
+
function systemdUnit(opts) {
|
|
234
|
+
const lines = [
|
|
235
|
+
"[Unit]",
|
|
236
|
+
`Description=${opts.description}`,
|
|
237
|
+
`After=network.target${opts.after?.length ? ` ${opts.after.join(" ")}` : ""}`
|
|
238
|
+
];
|
|
239
|
+
if (opts.requires?.length)
|
|
240
|
+
lines.push(`Requires=${opts.requires.join(" ")}`);
|
|
241
|
+
lines.push("", "[Service]", "Type=simple", `User=${VITESS_USER}`, `Group=${VITESS_USER}`, `Environment="PATH=${UNIT_PATH}"`, `Environment="VTDATAROOT=${VITESS_ROOT}"`, ...Object.entries(opts.environment ?? {}).map(([k, v]) => `Environment="${k}=${v}"`), ...(opts.execStartPre ?? []).map((c) => `ExecStartPre=${c}`), `ExecStart=${opts.execStart}`, "Restart=always", "RestartSec=5", "LimitNOFILE=65535", "", "[Install]", "WantedBy=multi-user.target");
|
|
242
|
+
return lines.join(`
|
|
243
|
+
`);
|
|
244
|
+
}
|
|
245
|
+
function writeUnit(name, body) {
|
|
246
|
+
return [
|
|
247
|
+
`cat > /etc/systemd/system/${name}.service <<'TS_CLOUD_VITESS_UNIT_EOF'`,
|
|
248
|
+
body,
|
|
249
|
+
"TS_CLOUD_VITESS_UNIT_EOF"
|
|
250
|
+
];
|
|
251
|
+
}
|
|
252
|
+
function buildVtcomboUnit(config) {
|
|
253
|
+
const cell = config.cell ?? "zone1";
|
|
254
|
+
const port = config.vtgatePort ?? VTGATE_MYSQL_PORT;
|
|
255
|
+
const keyspaces = ((config.keyspaces?.length) ? config.keyspaces : [{ name: "app" }]).map((k) => {
|
|
256
|
+
const shards = (k.sharded ? ["-80", "80-"] : ["0"]).map((s) => `shards:{name:"${s}"}`).join(" ");
|
|
257
|
+
return `keyspaces:{name:"${k.name}" ${shards}}`;
|
|
258
|
+
}).join(" ");
|
|
259
|
+
const topology = `${keyspaces} cells:"${cell}"`;
|
|
260
|
+
return systemdUnit({
|
|
261
|
+
description: "Vitess (vtcombo, single-process development stack)",
|
|
262
|
+
execStart: launcherFor("vtcombo", [
|
|
263
|
+
`${PANTRY_BIN}/vtcombo`,
|
|
264
|
+
`--cell ${cell}`,
|
|
265
|
+
`--proto-topo ${sh2(topology)}`,
|
|
266
|
+
`--mysql-server-port ${port}`,
|
|
267
|
+
"--mysql-server-bind-address 127.0.0.1",
|
|
268
|
+
"--mysql-auth-server-impl none",
|
|
269
|
+
"--start-mysql",
|
|
270
|
+
`--port ${VTGATE_GRPC_PORT}`,
|
|
271
|
+
`--grpc-port ${VTCTLD_GRPC_PORT}`
|
|
272
|
+
])
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
function buildEtcdUnit() {
|
|
276
|
+
return systemdUnit({
|
|
277
|
+
description: "etcd (Vitess topology store)",
|
|
278
|
+
execStart: launcherFor("etcd", [
|
|
279
|
+
`${PANTRY_BIN}/etcd`,
|
|
280
|
+
`--data-dir ${VITESS_ROOT}/etcd`,
|
|
281
|
+
`--listen-client-urls http://127.0.0.1:${ETCD_CLIENT_PORT}`,
|
|
282
|
+
`--advertise-client-urls http://127.0.0.1:${ETCD_CLIENT_PORT}`
|
|
283
|
+
]),
|
|
284
|
+
execStartPre: [`+/bin/mkdir -p ${VITESS_ROOT}/etcd`, `+/bin/chown ${VITESS_USER}:${VITESS_USER} ${VITESS_ROOT}/etcd`]
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
function topoFlags(config) {
|
|
288
|
+
const endpoint = config.etcdEndpoint ?? `http://127.0.0.1:${ETCD_CLIENT_PORT}`;
|
|
289
|
+
return [
|
|
290
|
+
"--topo-implementation etcd2",
|
|
291
|
+
`--topo-global-server-address ${endpoint}`,
|
|
292
|
+
"--topo-global-root /vitess/global"
|
|
293
|
+
];
|
|
294
|
+
}
|
|
295
|
+
function buildVtctldUnit(config) {
|
|
296
|
+
const requiresEtcd = !config.etcdEndpoint;
|
|
297
|
+
return systemdUnit({
|
|
298
|
+
description: "Vitess vtctld (control plane)",
|
|
299
|
+
after: requiresEtcd ? ["vitess-etcd.service"] : [],
|
|
300
|
+
requires: requiresEtcd ? ["vitess-etcd.service"] : [],
|
|
301
|
+
execStart: launcherFor("vtctld", [
|
|
302
|
+
`${PANTRY_BIN}/vtctld`,
|
|
303
|
+
...topoFlags(config),
|
|
304
|
+
`--cell ${config.cell ?? "zone1"}`,
|
|
305
|
+
`--service-map grpc-vtctl,grpc-vtctld`,
|
|
306
|
+
`--grpc-port ${VTCTLD_GRPC_PORT}`,
|
|
307
|
+
`--port ${VTCTLD_GRPC_PORT + 1}`
|
|
308
|
+
])
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
function buildVttabletUnit(config, keyspace, shard) {
|
|
312
|
+
const cell = config.cell ?? "zone1";
|
|
313
|
+
return systemdUnit({
|
|
314
|
+
description: `Vitess vttablet (${keyspace}/${shard})`,
|
|
315
|
+
after: ["vitess-vtctld.service", "vitess-mysqlctld.service"],
|
|
316
|
+
requires: ["vitess-mysqlctld.service"],
|
|
317
|
+
execStart: launcherFor("vttablet", [
|
|
318
|
+
`${PANTRY_BIN}/vttablet`,
|
|
319
|
+
...topoFlags(config),
|
|
320
|
+
`--tablet-path ${cell}-${String(VITESS_TABLET_UID).padStart(10, "0")}`,
|
|
321
|
+
`--init-keyspace ${keyspace}`,
|
|
322
|
+
`--init-shard ${shard}`,
|
|
323
|
+
"--init-tablet-type replica",
|
|
324
|
+
`--service-map ${sh2("grpc-queryservice,grpc-tabletmanager,grpc-updatestream")}`,
|
|
325
|
+
`--port ${VTTABLET_GRPC_PORT}`,
|
|
326
|
+
`--grpc-port ${VTTABLET_GRPC_PORT + 1}`,
|
|
327
|
+
`--db-port ${config.mysqlPort ?? 3306}`,
|
|
328
|
+
`--mycnf-mysql-port ${config.mysqlPort ?? 3306}`
|
|
329
|
+
])
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
function buildMysqlctldUnit(config) {
|
|
333
|
+
return systemdUnit({
|
|
334
|
+
description: "Vitess mysqlctld (managed mysqld)",
|
|
335
|
+
execStart: launcherFor("mysqlctld", [
|
|
336
|
+
`${PANTRY_BIN}/mysqlctld`,
|
|
337
|
+
`--tablet-uid ${VITESS_TABLET_UID}`,
|
|
338
|
+
`--mysql-port ${config.mysqlPort ?? 3306}`,
|
|
339
|
+
'--init-db-sql-file "$VTROOT/config/init_db.sql"',
|
|
340
|
+
"--wait-time 2m"
|
|
341
|
+
]),
|
|
342
|
+
execStartPre: [`+/bin/mkdir -p ${VITESS_ROOT}`, `+/bin/chown ${VITESS_USER}:${VITESS_USER} ${VITESS_ROOT}`]
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
var VITESS_AUTH_FILE = "/etc/vitess/auth.json";
|
|
346
|
+
function buildVitessAuthFileScript(config) {
|
|
347
|
+
const user = config.username ?? "vitess";
|
|
348
|
+
const password = config.password ?? "";
|
|
349
|
+
const auth = JSON.stringify({ [user]: [{ MysqlNativePassword: "", Password: password, UserData: user }] });
|
|
350
|
+
return [
|
|
351
|
+
"mkdir -p /etc/vitess",
|
|
352
|
+
`cat > ${VITESS_AUTH_FILE} <<'TS_CLOUD_VITESS_AUTH_EOF'`,
|
|
353
|
+
auth,
|
|
354
|
+
"TS_CLOUD_VITESS_AUTH_EOF",
|
|
355
|
+
`chown ${VITESS_USER}:${VITESS_USER} ${VITESS_AUTH_FILE}`,
|
|
356
|
+
`chmod 0600 ${VITESS_AUTH_FILE}`
|
|
357
|
+
];
|
|
358
|
+
}
|
|
359
|
+
function buildVtgateUnit(config) {
|
|
360
|
+
const cell = config.cell ?? "zone1";
|
|
361
|
+
return systemdUnit({
|
|
362
|
+
description: "Vitess vtgate (query router)",
|
|
363
|
+
after: ["vitess-vtctld.service"],
|
|
364
|
+
execStart: launcherFor("vtgate", [
|
|
365
|
+
`${PANTRY_BIN}/vtgate`,
|
|
366
|
+
...topoFlags(config),
|
|
367
|
+
`--cell ${cell}`,
|
|
368
|
+
`--cells-to-watch ${cell}`,
|
|
369
|
+
"--tablet-types-to-wait PRIMARY,REPLICA",
|
|
370
|
+
`--service-map ${sh2("grpc-vtgateservice")}`,
|
|
371
|
+
`--mysql-server-port ${config.vtgatePort ?? VTGATE_MYSQL_PORT}`,
|
|
372
|
+
`--mysql-server-bind-address ${config.bindAddress ?? "127.0.0.1"}`,
|
|
373
|
+
"--mysql-auth-server-impl static",
|
|
374
|
+
`--mysql-auth-server-static-file ${VITESS_AUTH_FILE}`,
|
|
375
|
+
`--port ${VTGATE_GRPC_PORT}`,
|
|
376
|
+
`--grpc-port ${VTGATE_GRPC_PORT + 1}`
|
|
377
|
+
])
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
function buildVitessBootstrapScript(config) {
|
|
381
|
+
const cell = config.cell ?? "zone1";
|
|
382
|
+
const endpoint = config.etcdEndpoint ?? `http://127.0.0.1:${ETCD_CLIENT_PORT}`;
|
|
383
|
+
const client = `${PANTRY_BIN}/vtctldclient --server 127.0.0.1:${VTCTLD_GRPC_PORT}`;
|
|
384
|
+
const out = [
|
|
385
|
+
`for i in $(seq 1 30); do ${client} GetCellInfoNames >/dev/null 2>&1 && break; sleep 2; done`,
|
|
386
|
+
`if ! ${client} GetCellInfoNames 2>/dev/null | grep -qx ${sh2(cell)}; then`,
|
|
387
|
+
` ${client} AddCellInfo --root ${sh2(`/vitess/${cell}`)} --server-address ${sh2(endpoint)} ${sh2(cell)}`,
|
|
388
|
+
"fi"
|
|
389
|
+
];
|
|
390
|
+
for (const keyspace of config.keyspaces ?? []) {
|
|
391
|
+
const sharded = keyspace.sharded ? " --sharded" : "";
|
|
392
|
+
out.push(`if ! ${client} GetKeyspace ${sh2(keyspace.name)} >/dev/null 2>&1; then`, ` ${client} CreateKeyspace${sharded} ${sh2(keyspace.name)}`, "fi");
|
|
393
|
+
const alias = `${cell}-${String(VITESS_TABLET_UID).padStart(10, "0")}`;
|
|
394
|
+
for (const shard of keyspace.sharded ? ["-80", "80-"] : ["0"]) {
|
|
395
|
+
const target = `${keyspace.name}/${shard}`;
|
|
396
|
+
out.push(`for i in $(seq 1 30); do ${client} GetTablet ${sh2(alias)} >/dev/null 2>&1 && break; sleep 2; done`, `if ! ${client} GetShard ${sh2(target)} 2>/dev/null | grep -q '"primary_alias"'; then`, ` ${client} PlannedReparentShard ${sh2(target)} --new-primary ${sh2(alias)} || ${client} InitShardPrimary --force ${sh2(target)} ${sh2(alias)}`, "fi");
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
return out;
|
|
400
|
+
}
|
|
401
|
+
function buildVitessHealthCheck(config) {
|
|
402
|
+
const port = config.vtgatePort ?? VTGATE_MYSQL_PORT;
|
|
403
|
+
return [
|
|
404
|
+
`for i in $(seq 1 60); do`,
|
|
405
|
+
` if (cd ${PANTRY_PROJECT_DIR} && eval "$(pantry env 2>/dev/null)"; MYSQL_PWD=${sh2(config.password ?? "")} ${PANTRY_BIN}/mysql -h 127.0.0.1 -P ${port} -u ${sh2(config.username ?? "vitess")} --connect-timeout=2 -e 'SHOW KEYSPACES') >/dev/null 2>&1; then`,
|
|
406
|
+
` echo "vtgate is serving on ${port}"; exit 0`,
|
|
407
|
+
" fi",
|
|
408
|
+
" sleep 2",
|
|
409
|
+
"done",
|
|
410
|
+
`echo "vtgate did not become ready on port ${port}" >&2`,
|
|
411
|
+
"exit 1"
|
|
412
|
+
];
|
|
413
|
+
}
|
|
414
|
+
function buildVitessProvisionScript(value) {
|
|
415
|
+
if (!enabled(value))
|
|
416
|
+
return [];
|
|
417
|
+
const config = settings(value);
|
|
418
|
+
const combo = config.mode === "combo";
|
|
419
|
+
launchers.clear();
|
|
420
|
+
const out = [
|
|
421
|
+
...buildPantryInstallScript(vitessPackages(config)),
|
|
422
|
+
`id -u ${VITESS_USER} >/dev/null 2>&1 || useradd --system --home-dir ${VITESS_ROOT} --shell /usr/sbin/nologin ${VITESS_USER}`,
|
|
423
|
+
`mkdir -p ${VITESS_ROOT}`,
|
|
424
|
+
`chown -R ${VITESS_USER}:${VITESS_USER} ${VITESS_ROOT}`
|
|
425
|
+
];
|
|
426
|
+
if (combo) {
|
|
427
|
+
out.push(...writeUnit("vitess-vtcombo", buildVtcomboUnit(config)), ...buildLauncherScripts(), "systemctl daemon-reload", "systemctl enable --now vitess-vtcombo.service", ...buildVitessHealthCheck(config));
|
|
428
|
+
return out;
|
|
429
|
+
}
|
|
430
|
+
out.push(...buildVitessAuthFileScript(config));
|
|
431
|
+
const units = [];
|
|
432
|
+
if (!config.etcdEndpoint)
|
|
433
|
+
units.push(["vitess-etcd", buildEtcdUnit()]);
|
|
434
|
+
units.push(["vitess-vtctld", buildVtctldUnit(config)], ["vitess-mysqlctld", buildMysqlctldUnit(config)], ["vitess-vtgate", buildVtgateUnit(config)]);
|
|
435
|
+
for (const keyspace of config.keyspaces ?? []) {
|
|
436
|
+
for (const shard of keyspace.sharded ? ["-80", "80-"] : ["0"])
|
|
437
|
+
units.push([`vitess-vttablet-${keyspace.name}-${shard.replace(/-/g, "x")}`, buildVttabletUnit(config, keyspace.name, shard)]);
|
|
438
|
+
}
|
|
439
|
+
for (const [name, body] of units)
|
|
440
|
+
out.push(...writeUnit(name, body));
|
|
441
|
+
out.push(...buildLauncherScripts());
|
|
442
|
+
out.push("systemctl daemon-reload");
|
|
443
|
+
for (const [name] of units)
|
|
444
|
+
out.push(`systemctl enable --now ${name}.service`);
|
|
445
|
+
out.push(...buildVitessBootstrapScript(config), ...buildVitessHealthCheck(config));
|
|
446
|
+
return out;
|
|
447
|
+
}
|
|
448
|
+
|
|
174
449
|
// src/drivers/shared/db-provision.ts
|
|
450
|
+
var ALWAYS_EXTERNAL_ENGINES = new Set(["singlestore", "vitess"]);
|
|
175
451
|
function isLocalDatabase(database) {
|
|
452
|
+
if (database?.engine && ALWAYS_EXTERNAL_ENGINES.has(database.engine))
|
|
453
|
+
return false;
|
|
176
454
|
return !database?.host || database.host === "127.0.0.1" || database.host === "localhost";
|
|
177
455
|
}
|
|
178
456
|
function pgAdminCommand(database, tool = "psql") {
|
|
@@ -183,32 +461,32 @@ function pgAdminCommand(database, tool = "psql") {
|
|
|
183
461
|
const env = database?.password ? `PGPASSWORD='${database.password.replace(/'/g, `'\\''`)}' ` : "";
|
|
184
462
|
return `${env}${tool} -h ${database.host} -p ${port} -U ${user} -w`;
|
|
185
463
|
}
|
|
186
|
-
function
|
|
464
|
+
function enabled2(spec) {
|
|
187
465
|
return spec === true || typeof spec === "object" && spec != null;
|
|
188
466
|
}
|
|
189
467
|
function planServices(services) {
|
|
190
468
|
const packages = [];
|
|
191
469
|
const names = [];
|
|
192
|
-
if (
|
|
470
|
+
if (enabled2(services.mysql)) {
|
|
193
471
|
packages.push(typeof services.mysql === "object" && services.mysql.version ? `mysql.com@${services.mysql.version}` : "mysql.com@8.0.43");
|
|
194
472
|
names.push("mysql");
|
|
195
|
-
} else if (
|
|
473
|
+
} else if (enabled2(services.mariadb)) {
|
|
196
474
|
packages.push(PANTRY_PACKAGES.mariadb);
|
|
197
475
|
names.push("mariadb");
|
|
198
476
|
}
|
|
199
|
-
if (
|
|
477
|
+
if (enabled2(services.postgres)) {
|
|
200
478
|
packages.push("postgresql.org");
|
|
201
479
|
names.push("postgres");
|
|
202
480
|
}
|
|
203
|
-
if (
|
|
481
|
+
if (enabled2(services.redis)) {
|
|
204
482
|
packages.push("redis.io");
|
|
205
483
|
names.push("redis");
|
|
206
484
|
}
|
|
207
|
-
if (
|
|
485
|
+
if (enabled2(services.memcached)) {
|
|
208
486
|
packages.push("memcached.org");
|
|
209
487
|
names.push("memcached");
|
|
210
488
|
}
|
|
211
|
-
if (
|
|
489
|
+
if (enabled2(services.meilisearch)) {
|
|
212
490
|
packages.push("meilisearch.com");
|
|
213
491
|
names.push("meilisearch");
|
|
214
492
|
}
|
|
@@ -216,9 +494,14 @@ function planServices(services) {
|
|
|
216
494
|
}
|
|
217
495
|
function buildServicesProvisionScript(services = {}, _options = {}) {
|
|
218
496
|
const plan = planServices(services);
|
|
497
|
+
const vitess = buildVitessProvisionScript(services.vitess);
|
|
219
498
|
if (plan.packages.length === 0)
|
|
220
|
-
return
|
|
221
|
-
return [
|
|
499
|
+
return vitess;
|
|
500
|
+
return [
|
|
501
|
+
...buildPantryInstallScript(plan.packages),
|
|
502
|
+
...buildPantryServiceScript(plan.services),
|
|
503
|
+
...vitess
|
|
504
|
+
];
|
|
222
505
|
}
|
|
223
506
|
function buildDatabaseSetupScript(database, services = {}) {
|
|
224
507
|
if (!database?.name)
|
|
@@ -228,9 +511,9 @@ function buildDatabaseSetupScript(database, services = {}) {
|
|
|
228
511
|
const name = database.name;
|
|
229
512
|
const user = database.username || name;
|
|
230
513
|
const pass = database.password || "";
|
|
231
|
-
const usePostgres =
|
|
232
|
-
const useMariadb =
|
|
233
|
-
const useMysql =
|
|
514
|
+
const usePostgres = enabled2(services.postgres) || database.engine === "postgres";
|
|
515
|
+
const useMariadb = enabled2(services.mariadb) || database.engine === "mariadb";
|
|
516
|
+
const useMysql = enabled2(services.mysql) || database.engine === "mysql";
|
|
234
517
|
if (usePostgres && !useMysql && !useMariadb) {
|
|
235
518
|
const pgIdent = (v) => `"${v.replace(/"/g, '""')}"`;
|
|
236
519
|
const pgLit = (v) => `'${v.replace(/'/g, "''")}'`;
|
|
@@ -300,8 +583,9 @@ function buildManagedDbEnv(database) {
|
|
|
300
583
|
if (!database?.name)
|
|
301
584
|
return {};
|
|
302
585
|
const isSingleStore = database.engine === "singlestore";
|
|
303
|
-
const
|
|
304
|
-
const
|
|
586
|
+
const isVitess = database.engine === "vitess";
|
|
587
|
+
const connection = database.engine === "postgres" ? "pgsql" : isSingleStore ? "singlestore" : isVitess ? "vitess" : "mysql";
|
|
588
|
+
const port = database.port ?? (database.engine === "postgres" ? 5432 : isVitess ? 15306 : 3306);
|
|
305
589
|
const env = {
|
|
306
590
|
DB_CONNECTION: connection,
|
|
307
591
|
DB_HOST: database.host || "127.0.0.1",
|
|
@@ -312,7 +596,7 @@ function buildManagedDbEnv(database) {
|
|
|
312
596
|
env.DB_USERNAME = database.username;
|
|
313
597
|
if (database.password)
|
|
314
598
|
env.DB_PASSWORD = database.password;
|
|
315
|
-
if (isSingleStore && database.ssl !== false)
|
|
599
|
+
if ((isSingleStore || isVitess) && database.ssl !== false)
|
|
316
600
|
env.DB_SSL = "true";
|
|
317
601
|
return env;
|
|
318
602
|
}
|
|
@@ -433,8 +717,8 @@ function buildBackupRestoreScript(database, options = {}) {
|
|
|
433
717
|
}
|
|
434
718
|
|
|
435
719
|
// src/drivers/shared/maintenance.ts
|
|
436
|
-
function buildAutoUpdatesScript(
|
|
437
|
-
if (!
|
|
720
|
+
function buildAutoUpdatesScript(enabled3 = true) {
|
|
721
|
+
if (!enabled3)
|
|
438
722
|
return [];
|
|
439
723
|
return [
|
|
440
724
|
"export DEBIAN_FRONTEND=noninteractive",
|
|
@@ -468,17 +752,17 @@ var SERVICE_PROBES = [
|
|
|
468
752
|
];
|
|
469
753
|
function resolveMonitoring(monitoring = true) {
|
|
470
754
|
const obj = typeof monitoring === "object" ? monitoring : {};
|
|
471
|
-
const
|
|
755
|
+
const enabled3 = typeof monitoring === "boolean" ? monitoring : monitoring.enabled !== false;
|
|
472
756
|
return {
|
|
473
|
-
enabled:
|
|
757
|
+
enabled: enabled3,
|
|
474
758
|
cpuLoadPerCore: obj.alerts?.cpuLoadPerCore ?? DEFAULT_CPU_LOAD_PER_CORE,
|
|
475
759
|
memPercent: obj.alerts?.memPercent ?? DEFAULT_MEM_PERCENT,
|
|
476
760
|
diskPercent: obj.alerts?.diskPercent ?? DEFAULT_DISK_PERCENT
|
|
477
761
|
};
|
|
478
762
|
}
|
|
479
763
|
function buildMonitoringScript(monitoring = true) {
|
|
480
|
-
const { enabled:
|
|
481
|
-
if (!
|
|
764
|
+
const { enabled: enabled3, cpuLoadPerCore, memPercent, diskPercent } = resolveMonitoring(monitoring);
|
|
765
|
+
if (!enabled3)
|
|
482
766
|
return [];
|
|
483
767
|
const probeLines = SERVICE_PROBES.map(([name, port]) => `SVC_${name.toUpperCase()}=$(probe ${port})`);
|
|
484
768
|
const servicesJson = SERVICE_PROBES.map(([name]) => `"${name}":"$SVC_${name.toUpperCase()}"`).join(",");
|
|
@@ -765,7 +1049,7 @@ function buildNginxVhost(options) {
|
|
|
765
1049
|
}
|
|
766
1050
|
function buildNginxVhostScript(options) {
|
|
767
1051
|
const available = `/etc/nginx/sites-available/${options.siteName}`;
|
|
768
|
-
const
|
|
1052
|
+
const enabled3 = `/etc/nginx/sites-enabled/${options.siteName}`;
|
|
769
1053
|
const vhost = buildNginxVhost(options);
|
|
770
1054
|
const out = [...buildNginxServiceScript()];
|
|
771
1055
|
if (options.auth) {
|
|
@@ -775,7 +1059,7 @@ function buildNginxVhostScript(options) {
|
|
|
775
1059
|
const user = sq(options.auth.username);
|
|
776
1060
|
out.push(`TS_CLOUD_HTPASS=$(openssl passwd -apr1 '${pw}')`, `printf '%s:%s\\n' '${user}' "$TS_CLOUD_HTPASS" > ${file}`, `chmod 640 ${file}`, `chown root:www-data ${file} 2>/dev/null || true`);
|
|
777
1061
|
}
|
|
778
|
-
out.push(`cat > ${available} <<'TS_CLOUD_NGINX_EOF'`, vhost.replace(/\n$/, ""), "TS_CLOUD_NGINX_EOF", `ln -sf ${available} ${
|
|
1062
|
+
out.push(`cat > ${available} <<'TS_CLOUD_NGINX_EOF'`, vhost.replace(/\n$/, ""), "TS_CLOUD_NGINX_EOF", `ln -sf ${available} ${enabled3}`, "rm -f /etc/nginx/sites-enabled/default", `${NGINX_WRAPPER} -t`, "systemctl reload ts-cloud-nginx 2>/dev/null || systemctl restart ts-cloud-nginx 2>/dev/null || true");
|
|
779
1063
|
return out;
|
|
780
1064
|
}
|
|
781
1065
|
var NGINX_WRAPPER = "/usr/local/bin/ts-cloud-nginx";
|
|
@@ -2516,6 +2800,65 @@ class HetznerClient {
|
|
|
2516
2800
|
});
|
|
2517
2801
|
return data.action;
|
|
2518
2802
|
}
|
|
2803
|
+
async powerOffServer(serverId) {
|
|
2804
|
+
const data = await this.request("POST", `/servers/${serverId}/actions/poweroff`, {});
|
|
2805
|
+
return data.action;
|
|
2806
|
+
}
|
|
2807
|
+
async stopServer(serverId, options) {
|
|
2808
|
+
const server = await this.getServer(serverId);
|
|
2809
|
+
if (server.status === "off")
|
|
2810
|
+
return;
|
|
2811
|
+
await this.shutdownServer(serverId);
|
|
2812
|
+
const grace = options?.gracefulWaitMs ?? 120000;
|
|
2813
|
+
const poll = options?.pollIntervalMs ?? 3000;
|
|
2814
|
+
const start = Date.now();
|
|
2815
|
+
while (Date.now() - start < grace) {
|
|
2816
|
+
if ((await this.getServer(serverId)).status === "off")
|
|
2817
|
+
return;
|
|
2818
|
+
await new Promise((resolve) => setTimeout(resolve, poll));
|
|
2819
|
+
}
|
|
2820
|
+
const action = await this.powerOffServer(serverId);
|
|
2821
|
+
await this.waitForAction(action.id);
|
|
2822
|
+
await this.waitForServerStatus(serverId, "off");
|
|
2823
|
+
}
|
|
2824
|
+
async renameServer(serverId, name) {
|
|
2825
|
+
const data = await this.request("PUT", `/servers/${serverId}`, { name });
|
|
2826
|
+
return data.server;
|
|
2827
|
+
}
|
|
2828
|
+
async listImages(options) {
|
|
2829
|
+
const query = options?.type ? `?type=${encodeURIComponent(options.type)}` : "";
|
|
2830
|
+
return this.requestAll(`/images${query}`, "images");
|
|
2831
|
+
}
|
|
2832
|
+
async getImage(id) {
|
|
2833
|
+
const data = await this.request("GET", `/images/${id}`);
|
|
2834
|
+
return data.image;
|
|
2835
|
+
}
|
|
2836
|
+
async createImage(serverId, options) {
|
|
2837
|
+
return this.request("POST", `/servers/${serverId}/actions/create_image`, { description: options.description, type: options.type ?? "snapshot", labels: options.labels });
|
|
2838
|
+
}
|
|
2839
|
+
async deleteImage(imageId) {
|
|
2840
|
+
await this.request("DELETE", `/images/${imageId}`);
|
|
2841
|
+
}
|
|
2842
|
+
async rebuildServer(serverId, image) {
|
|
2843
|
+
const data = await this.request("POST", `/servers/${serverId}/actions/rebuild`, {
|
|
2844
|
+
image
|
|
2845
|
+
});
|
|
2846
|
+
return data.action;
|
|
2847
|
+
}
|
|
2848
|
+
async listPrimaryIps() {
|
|
2849
|
+
return this.requestAll("/primary_ips", "primary_ips");
|
|
2850
|
+
}
|
|
2851
|
+
async unassignPrimaryIp(ipId) {
|
|
2852
|
+
const data = await this.request("POST", `/primary_ips/${ipId}/actions/unassign`, {});
|
|
2853
|
+
return data.action;
|
|
2854
|
+
}
|
|
2855
|
+
async assignPrimaryIp(ipId, serverId) {
|
|
2856
|
+
const data = await this.request("POST", `/primary_ips/${ipId}/actions/assign`, {
|
|
2857
|
+
assignee_id: serverId,
|
|
2858
|
+
assignee_type: "server"
|
|
2859
|
+
});
|
|
2860
|
+
return data.action;
|
|
2861
|
+
}
|
|
2519
2862
|
async waitForAction(actionId, options) {
|
|
2520
2863
|
const pollInterval = options?.pollIntervalMs ?? 2000;
|
|
2521
2864
|
const maxWait = options?.maxWaitMs ?? 300000;
|
|
@@ -2696,16 +3039,16 @@ class HetznerDriver {
|
|
|
2696
3039
|
this.client = options.client ?? new HetznerClient({
|
|
2697
3040
|
apiToken: apiToken ?? ""
|
|
2698
3041
|
});
|
|
2699
|
-
const
|
|
3042
|
+
const settings2 = resolveHetznerSettings(undefined, {
|
|
2700
3043
|
sshPrivateKeyPath: options.sshPrivateKeyPath,
|
|
2701
3044
|
sshPublicKeyPath: options.sshPublicKeyPath,
|
|
2702
3045
|
sshUser: options.sshUser,
|
|
2703
3046
|
location: options.location
|
|
2704
3047
|
});
|
|
2705
|
-
this.sshPrivateKeyPath =
|
|
2706
|
-
this.sshPublicKeyPath =
|
|
2707
|
-
this.sshUser =
|
|
2708
|
-
this.location =
|
|
3048
|
+
this.sshPrivateKeyPath = settings2.sshPrivateKeyPath;
|
|
3049
|
+
this.sshPublicKeyPath = settings2.sshPublicKeyPath;
|
|
3050
|
+
this.sshUser = settings2.sshUser;
|
|
3051
|
+
this.location = settings2.location;
|
|
2709
3052
|
this.waitForBoot = options.waitForBoot ?? true;
|
|
2710
3053
|
this.bootWait = {
|
|
2711
3054
|
sshIntervalMs: options.bootWait?.sshIntervalMs ?? 5000,
|
|
@@ -4690,6 +5033,9 @@ function deployHistoryPath(base) {
|
|
|
4690
5033
|
function deployLogPath(base, releaseId) {
|
|
4691
5034
|
return `${deployMetaDir(base)}/deploys/${releaseId}.log`;
|
|
4692
5035
|
}
|
|
5036
|
+
function sharedPathsManifestPath(base) {
|
|
5037
|
+
return `${deployMetaDir(base)}/shared-paths`;
|
|
5038
|
+
}
|
|
4693
5039
|
function releasePaths(base, releaseId) {
|
|
4694
5040
|
const root = base.replace(/\/+$/, "");
|
|
4695
5041
|
return {
|
|
@@ -4706,17 +5052,59 @@ function isFileSharedPath(p) {
|
|
|
4706
5052
|
return true;
|
|
4707
5053
|
return /\.[a-z0-9]+$/i.test(name);
|
|
4708
5054
|
}
|
|
5055
|
+
function sharedPathOf(entry) {
|
|
5056
|
+
return typeof entry === "string" ? entry : entry.path;
|
|
5057
|
+
}
|
|
5058
|
+
function dedupeSharedPaths(entries) {
|
|
5059
|
+
const byPath = new Map;
|
|
5060
|
+
for (const entry of entries)
|
|
5061
|
+
byPath.set(sharedPathOf(entry), entry);
|
|
5062
|
+
return [...byPath.values()];
|
|
5063
|
+
}
|
|
5064
|
+
function resolveSharedPath(paths, entry) {
|
|
5065
|
+
if (typeof entry === "string")
|
|
5066
|
+
return { path: entry, target: `${paths.shared}/${entry}`, seed: true };
|
|
5067
|
+
return {
|
|
5068
|
+
path: entry.path,
|
|
5069
|
+
target: entry.target ?? `${paths.shared}/${entry.path}`,
|
|
5070
|
+
seed: entry.seed !== false
|
|
5071
|
+
};
|
|
5072
|
+
}
|
|
5073
|
+
function buildAdoptSharedPathFn(paths) {
|
|
5074
|
+
return [
|
|
5075
|
+
"ts_cloud_adopt_shared() {",
|
|
5076
|
+
' TS_CLOUD_SP="$1"',
|
|
5077
|
+
' TS_CLOUD_DST="$2"',
|
|
5078
|
+
` TS_CLOUD_SRC="${paths.current}/$TS_CLOUD_SP"`,
|
|
5079
|
+
' if [ -e "$TS_CLOUD_DST" ]; then return 0; fi',
|
|
5080
|
+
' if [ -L "$TS_CLOUD_SRC" ] || [ ! -e "$TS_CLOUD_SRC" ]; then return 0; fi',
|
|
5081
|
+
' mkdir -p "$(dirname "$TS_CLOUD_DST")"',
|
|
5082
|
+
' cp -a "$TS_CLOUD_SRC" "$TS_CLOUD_DST"',
|
|
5083
|
+
" for TS_CLOUD_SIDECAR in -wal -shm; do",
|
|
5084
|
+
' if [ -f "$TS_CLOUD_SRC$TS_CLOUD_SIDECAR" ]; then cp -a "$TS_CLOUD_SRC$TS_CLOUD_SIDECAR" "$TS_CLOUD_DST$TS_CLOUD_SIDECAR"; fi',
|
|
5085
|
+
" done",
|
|
5086
|
+
' echo "[ts-cloud] adopted $TS_CLOUD_SP from the live release into shared/ — it now survives deploys"',
|
|
5087
|
+
" return 0",
|
|
5088
|
+
"}"
|
|
5089
|
+
];
|
|
5090
|
+
}
|
|
4709
5091
|
function buildEnsureReleaseLayout(paths, sharedPaths = DEFAULT_SHARED_PATHS) {
|
|
4710
|
-
const lines = [`mkdir -p ${paths.releases} ${paths.shared}
|
|
4711
|
-
|
|
5092
|
+
const lines = [`mkdir -p ${paths.releases} ${paths.shared}`, ...buildAdoptSharedPathFn(paths)];
|
|
5093
|
+
const resolved = sharedPaths.map((entry) => resolveSharedPath(paths, entry));
|
|
5094
|
+
for (const { path: p, target, seed } of resolved) {
|
|
5095
|
+
if (!seed)
|
|
5096
|
+
continue;
|
|
5097
|
+
lines.push(`ts_cloud_adopt_shared '${p}' '${target}'`);
|
|
4712
5098
|
if (isFileSharedPath(p)) {
|
|
4713
|
-
lines.push(`mkdir -p "$(dirname ${
|
|
5099
|
+
lines.push(`mkdir -p "$(dirname ${target})"`, `touch ${target}`);
|
|
4714
5100
|
} else if (p === "storage") {
|
|
4715
|
-
lines.push(`mkdir -p ${
|
|
5101
|
+
lines.push(`mkdir -p ${target}/app/public`, `mkdir -p ${target}/framework/cache/data`, `mkdir -p ${target}/framework/sessions`, `mkdir -p ${target}/framework/testing`, `mkdir -p ${target}/framework/views`, `mkdir -p ${target}/logs`);
|
|
4716
5102
|
} else {
|
|
4717
|
-
lines.push(`mkdir -p ${
|
|
5103
|
+
lines.push(`mkdir -p ${target}`);
|
|
4718
5104
|
}
|
|
4719
5105
|
}
|
|
5106
|
+
const manifest = sharedPathsManifestPath(paths.base);
|
|
5107
|
+
lines.push(`mkdir -p ${deployMetaDir(paths.base)}`, `cat > ${manifest} <<'TS_CLOUD_SHARED_PATHS_EOF'`, ...resolved.map((entry) => `${entry.path} ${entry.target}`), "TS_CLOUD_SHARED_PATHS_EOF");
|
|
4720
5108
|
return lines;
|
|
4721
5109
|
}
|
|
4722
5110
|
function buildDeployLock(paths, waitSeconds = 900) {
|
|
@@ -4769,25 +5157,47 @@ function buildPromoteStagedRelease(paths) {
|
|
|
4769
5157
|
}
|
|
4770
5158
|
function buildLinkSharedPaths(paths, sharedPaths = DEFAULT_SHARED_PATHS) {
|
|
4771
5159
|
const lines = [];
|
|
4772
|
-
for (const
|
|
4773
|
-
const target =
|
|
5160
|
+
for (const entry of sharedPaths) {
|
|
5161
|
+
const { path: p, target, seed } = resolveSharedPath(paths, entry);
|
|
4774
5162
|
const link = `${paths.release}/${p}`;
|
|
4775
|
-
|
|
5163
|
+
const relink = [`rm -rf ${link}`, `mkdir -p "$(dirname ${link})"`, `ln -sfn ${target} ${link}`];
|
|
5164
|
+
if (seed) {
|
|
5165
|
+
lines.push(...relink);
|
|
5166
|
+
continue;
|
|
5167
|
+
}
|
|
5168
|
+
lines.push(`if [ -e ${target} ]; then`, ...relink.map((l) => ` ${l}`), "fi");
|
|
4776
5169
|
}
|
|
4777
5170
|
return lines;
|
|
4778
5171
|
}
|
|
5172
|
+
function buildRelinkSharedPaths(paths, releaseExpr) {
|
|
5173
|
+
const manifest = sharedPathsManifestPath(paths.base);
|
|
5174
|
+
return [
|
|
5175
|
+
`if [ -f ${manifest} ]; then`,
|
|
5176
|
+
' while IFS="\t" read -r TS_CLOUD_SP TS_CLOUD_TGT; do',
|
|
5177
|
+
' [ -n "$TS_CLOUD_SP" ] || continue',
|
|
5178
|
+
` [ -n "$TS_CLOUD_TGT" ] || TS_CLOUD_TGT="${paths.shared}/$TS_CLOUD_SP"`,
|
|
5179
|
+
' [ -e "$TS_CLOUD_TGT" ] || continue',
|
|
5180
|
+
` rm -rf ${releaseExpr}/"$TS_CLOUD_SP"`,
|
|
5181
|
+
` mkdir -p "$(dirname ${releaseExpr}/"$TS_CLOUD_SP")"`,
|
|
5182
|
+
` ln -sfn "$TS_CLOUD_TGT" ${releaseExpr}/"$TS_CLOUD_SP"`,
|
|
5183
|
+
` done < ${manifest}`,
|
|
5184
|
+
"fi"
|
|
5185
|
+
];
|
|
5186
|
+
}
|
|
4779
5187
|
function buildActivateRelease(paths) {
|
|
4780
5188
|
return [`ln -sfn ${paths.release} ${paths.current}.tmp`, `mv -Tf ${paths.current}.tmp ${paths.current}`];
|
|
4781
5189
|
}
|
|
4782
5190
|
function buildRollbackScript(paths, options = {}) {
|
|
4783
5191
|
const flip = options.to ? [
|
|
4784
5192
|
`[ -d ${paths.releases}/${options.to} ] || { echo "rollback target ${paths.releases}/${options.to} not found" >&2; exit 1; }`,
|
|
5193
|
+
...buildRelinkSharedPaths(paths, `${paths.releases}/${options.to}`),
|
|
4785
5194
|
`ln -sfn ${paths.releases}/${options.to} ${paths.current}.tmp`,
|
|
4786
5195
|
`mv -Tf ${paths.current}.tmp ${paths.current}`
|
|
4787
5196
|
] : [
|
|
4788
5197
|
`TS_CLOUD_CURRENT=$(readlink -f ${paths.current} 2>/dev/null || true)`,
|
|
4789
5198
|
`TS_CLOUD_PREV=$(ls -1dt ${paths.releases}/*/ 2>/dev/null | sed 's#/$##' | while read -r r; do ` + '[ "$(readlink -f "$r")" != "$TS_CLOUD_CURRENT" ] && { echo "$r"; break; }; done)',
|
|
4790
5199
|
'[ -n "$TS_CLOUD_PREV" ] || { echo "no previous release to roll back to" >&2; exit 1; }',
|
|
5200
|
+
...buildRelinkSharedPaths(paths, '"$TS_CLOUD_PREV"'),
|
|
4791
5201
|
`ln -sfn "$TS_CLOUD_PREV" ${paths.current}.tmp`,
|
|
4792
5202
|
`mv -Tf ${paths.current}.tmp ${paths.current}`,
|
|
4793
5203
|
'echo "rolled back to $TS_CLOUD_PREV"'
|
|
@@ -4875,7 +5285,7 @@ function buildSiteDeployScript(options) {
|
|
|
4875
5285
|
const unitBase = `${slug}-${siteName}`;
|
|
4876
5286
|
const serviceName = `${unitBase}.service`;
|
|
4877
5287
|
const tarball = releaseTarballTmpPath(slug, siteName, releaseId);
|
|
4878
|
-
const sharedPaths =
|
|
5288
|
+
const sharedPaths = dedupeSharedPaths([".env", ...options.sharedPaths ?? []]);
|
|
4879
5289
|
const envFile = formatEnvFile(envEntries);
|
|
4880
5290
|
const preStart = preStartCommands.length > 0 ? [`cd ${paths.release}`, ...preStartCommands] : [];
|
|
4881
5291
|
const stageRelease = [
|
|
@@ -5353,7 +5763,7 @@ function queueUnitName(slug, siteName, index) {
|
|
|
5353
5763
|
function daemonUnitName(slug, siteName, daemon, index) {
|
|
5354
5764
|
return `${slug}-${siteName}-daemon-${daemon.name ? slugify(daemon.name) : slugify(daemon.command).slice(0, 32) || String(index)}`;
|
|
5355
5765
|
}
|
|
5356
|
-
function
|
|
5766
|
+
function systemdUnit2(opts) {
|
|
5357
5767
|
const lines = [
|
|
5358
5768
|
"[Unit]",
|
|
5359
5769
|
`Description=${opts.description}`,
|
|
@@ -5400,7 +5810,7 @@ function buildSiteServicesScript(options) {
|
|
|
5400
5810
|
for (let p = 0;p < processes; p++) {
|
|
5401
5811
|
const name = queueUnitName(slug, siteName, desiredUnits.length);
|
|
5402
5812
|
desiredUnits.push(name);
|
|
5403
|
-
out.push(...writeUnitScript(name,
|
|
5813
|
+
out.push(...writeUnitScript(name, systemdUnit2({
|
|
5404
5814
|
description: `${siteName} queue worker ${qIndex}.${p} (managed by ts-cloud)`,
|
|
5405
5815
|
workingDir: current,
|
|
5406
5816
|
execStart: driver.wrapExec(driver.queueWorkerCommand(worker, ctx)),
|
|
@@ -5415,7 +5825,7 @@ function buildSiteServicesScript(options) {
|
|
|
5415
5825
|
for (let p = 0;p < processes; p++) {
|
|
5416
5826
|
const name = `${daemonUnitName(slug, siteName, daemon, dIndex)}-${p}`;
|
|
5417
5827
|
desiredUnits.push(name);
|
|
5418
|
-
out.push(...writeUnitScript(name,
|
|
5828
|
+
out.push(...writeUnitScript(name, systemdUnit2({
|
|
5419
5829
|
description: `${siteName} daemon ${daemon.name || daemon.command} (managed by ts-cloud)`,
|
|
5420
5830
|
workingDir: daemon.directory || current,
|
|
5421
5831
|
execStart: driver.wrapExec(daemon.command),
|
|
@@ -5430,7 +5840,7 @@ function buildSiteServicesScript(options) {
|
|
|
5430
5840
|
const schedulerUnit = `${slug}-${siteName}-scheduler`;
|
|
5431
5841
|
if (schedulerEnabled && driver.schedulerMode === "daemon") {
|
|
5432
5842
|
desiredUnits.push(schedulerUnit);
|
|
5433
|
-
out.push(...writeUnitScript(schedulerUnit,
|
|
5843
|
+
out.push(...writeUnitScript(schedulerUnit, systemdUnit2({
|
|
5434
5844
|
description: `${siteName} scheduler (managed by ts-cloud)`,
|
|
5435
5845
|
workingDir: current,
|
|
5436
5846
|
execStart: driver.wrapExec(driver.schedulerCommand(ctx)),
|