@stacksjs/ts-cloud 0.7.93 → 0.7.95
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 +1188 -1184
- package/dist/bin/dashboard-server.js +413 -409
- package/dist/{chunk-y5wrqyyz.js → chunk-3d4204h1.js} +365 -1
- package/dist/{chunk-fbwcv2vf.js → chunk-xp3txtg9.js} +458 -42
- 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/ui/access-denied.html +2 -2
- package/dist/ui/account/automation.html +3 -3
- package/dist/ui/applications/compose.html +4 -4
- package/dist/ui/applications/new.html +2 -2
- package/dist/ui/data/backups.html +4 -4
- package/dist/ui/data/services.html +4 -4
- package/dist/ui/data/volumes.html +4 -4
- 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 +4 -4
- 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 +3 -3
- package/dist/ui/server/database.html +123 -7
- package/dist/ui/server/deployments.html +4 -4
- 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/security.html +2 -2
- 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/assets.html +2 -2
- package/dist/ui/serverless/cost.html +2 -2
- package/dist/ui/serverless/data.html +4 -4
- 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/metrics.html +2 -2
- 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 +4 -4
- 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@8.0.43");
|
|
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";
|
|
@@ -1420,7 +1704,13 @@ function buildCertManagementCommands(options) {
|
|
|
1420
1704
|
`DOMAINS='${csv}'`,
|
|
1421
1705
|
'before=$(cat "$CERTS"/*.crt 2>/dev/null | sha256sum)',
|
|
1422
1706
|
`for d in ${spaced}; do`,
|
|
1423
|
-
' [ -s "$CERTS/$d.crt" ]
|
|
1707
|
+
' if [ ! -s "$CERTS/$d.crt" ]; then',
|
|
1708
|
+
' $TLSX acme:issue -d "$d" --method http-01 --webroot "$WEBROOT" --dir "$CERTS" --prod --email "$EMAIL" || echo "issue $d failed (non-fatal)"',
|
|
1709
|
+
" # rpx reloads its SNI set when a PEM appears. Complete that reload",
|
|
1710
|
+
" # before the next hostname starts http-01, or :80 can disappear in",
|
|
1711
|
+
" # the middle of the following challenge on a multi-domain deploy.",
|
|
1712
|
+
` [ -s "$CERTS/$d.crt" ] && systemctl restart ${RPX_SERVICE_NAME}`,
|
|
1713
|
+
" fi",
|
|
1424
1714
|
"done",
|
|
1425
1715
|
'$TLSX acme:renew --domains "$DOMAINS" --method http-01 --webroot "$WEBROOT" --dir "$CERTS" --days 30 --prod --email "$EMAIL" || echo "renew: some domains failed (non-fatal)"',
|
|
1426
1716
|
'rm -f "$CERTS"/*.chain.crt',
|
|
@@ -2516,6 +2806,65 @@ class HetznerClient {
|
|
|
2516
2806
|
});
|
|
2517
2807
|
return data.action;
|
|
2518
2808
|
}
|
|
2809
|
+
async powerOffServer(serverId) {
|
|
2810
|
+
const data = await this.request("POST", `/servers/${serverId}/actions/poweroff`, {});
|
|
2811
|
+
return data.action;
|
|
2812
|
+
}
|
|
2813
|
+
async stopServer(serverId, options) {
|
|
2814
|
+
const server = await this.getServer(serverId);
|
|
2815
|
+
if (server.status === "off")
|
|
2816
|
+
return;
|
|
2817
|
+
await this.shutdownServer(serverId);
|
|
2818
|
+
const grace = options?.gracefulWaitMs ?? 120000;
|
|
2819
|
+
const poll = options?.pollIntervalMs ?? 3000;
|
|
2820
|
+
const start = Date.now();
|
|
2821
|
+
while (Date.now() - start < grace) {
|
|
2822
|
+
if ((await this.getServer(serverId)).status === "off")
|
|
2823
|
+
return;
|
|
2824
|
+
await new Promise((resolve) => setTimeout(resolve, poll));
|
|
2825
|
+
}
|
|
2826
|
+
const action = await this.powerOffServer(serverId);
|
|
2827
|
+
await this.waitForAction(action.id);
|
|
2828
|
+
await this.waitForServerStatus(serverId, "off");
|
|
2829
|
+
}
|
|
2830
|
+
async renameServer(serverId, name) {
|
|
2831
|
+
const data = await this.request("PUT", `/servers/${serverId}`, { name });
|
|
2832
|
+
return data.server;
|
|
2833
|
+
}
|
|
2834
|
+
async listImages(options) {
|
|
2835
|
+
const query = options?.type ? `?type=${encodeURIComponent(options.type)}` : "";
|
|
2836
|
+
return this.requestAll(`/images${query}`, "images");
|
|
2837
|
+
}
|
|
2838
|
+
async getImage(id) {
|
|
2839
|
+
const data = await this.request("GET", `/images/${id}`);
|
|
2840
|
+
return data.image;
|
|
2841
|
+
}
|
|
2842
|
+
async createImage(serverId, options) {
|
|
2843
|
+
return this.request("POST", `/servers/${serverId}/actions/create_image`, { description: options.description, type: options.type ?? "snapshot", labels: options.labels });
|
|
2844
|
+
}
|
|
2845
|
+
async deleteImage(imageId) {
|
|
2846
|
+
await this.request("DELETE", `/images/${imageId}`);
|
|
2847
|
+
}
|
|
2848
|
+
async rebuildServer(serverId, image) {
|
|
2849
|
+
const data = await this.request("POST", `/servers/${serverId}/actions/rebuild`, {
|
|
2850
|
+
image
|
|
2851
|
+
});
|
|
2852
|
+
return data.action;
|
|
2853
|
+
}
|
|
2854
|
+
async listPrimaryIps() {
|
|
2855
|
+
return this.requestAll("/primary_ips", "primary_ips");
|
|
2856
|
+
}
|
|
2857
|
+
async unassignPrimaryIp(ipId) {
|
|
2858
|
+
const data = await this.request("POST", `/primary_ips/${ipId}/actions/unassign`, {});
|
|
2859
|
+
return data.action;
|
|
2860
|
+
}
|
|
2861
|
+
async assignPrimaryIp(ipId, serverId) {
|
|
2862
|
+
const data = await this.request("POST", `/primary_ips/${ipId}/actions/assign`, {
|
|
2863
|
+
assignee_id: serverId,
|
|
2864
|
+
assignee_type: "server"
|
|
2865
|
+
});
|
|
2866
|
+
return data.action;
|
|
2867
|
+
}
|
|
2519
2868
|
async waitForAction(actionId, options) {
|
|
2520
2869
|
const pollInterval = options?.pollIntervalMs ?? 2000;
|
|
2521
2870
|
const maxWait = options?.maxWaitMs ?? 300000;
|
|
@@ -2696,16 +3045,16 @@ class HetznerDriver {
|
|
|
2696
3045
|
this.client = options.client ?? new HetznerClient({
|
|
2697
3046
|
apiToken: apiToken ?? ""
|
|
2698
3047
|
});
|
|
2699
|
-
const
|
|
3048
|
+
const settings2 = resolveHetznerSettings(undefined, {
|
|
2700
3049
|
sshPrivateKeyPath: options.sshPrivateKeyPath,
|
|
2701
3050
|
sshPublicKeyPath: options.sshPublicKeyPath,
|
|
2702
3051
|
sshUser: options.sshUser,
|
|
2703
3052
|
location: options.location
|
|
2704
3053
|
});
|
|
2705
|
-
this.sshPrivateKeyPath =
|
|
2706
|
-
this.sshPublicKeyPath =
|
|
2707
|
-
this.sshUser =
|
|
2708
|
-
this.location =
|
|
3054
|
+
this.sshPrivateKeyPath = settings2.sshPrivateKeyPath;
|
|
3055
|
+
this.sshPublicKeyPath = settings2.sshPublicKeyPath;
|
|
3056
|
+
this.sshUser = settings2.sshUser;
|
|
3057
|
+
this.location = settings2.location;
|
|
2709
3058
|
this.waitForBoot = options.waitForBoot ?? true;
|
|
2710
3059
|
this.bootWait = {
|
|
2711
3060
|
sshIntervalMs: options.bootWait?.sshIntervalMs ?? 5000,
|
|
@@ -4690,6 +5039,9 @@ function deployHistoryPath(base) {
|
|
|
4690
5039
|
function deployLogPath(base, releaseId) {
|
|
4691
5040
|
return `${deployMetaDir(base)}/deploys/${releaseId}.log`;
|
|
4692
5041
|
}
|
|
5042
|
+
function sharedPathsManifestPath(base) {
|
|
5043
|
+
return `${deployMetaDir(base)}/shared-paths`;
|
|
5044
|
+
}
|
|
4693
5045
|
function releasePaths(base, releaseId) {
|
|
4694
5046
|
const root = base.replace(/\/+$/, "");
|
|
4695
5047
|
return {
|
|
@@ -4706,17 +5058,59 @@ function isFileSharedPath(p) {
|
|
|
4706
5058
|
return true;
|
|
4707
5059
|
return /\.[a-z0-9]+$/i.test(name);
|
|
4708
5060
|
}
|
|
5061
|
+
function sharedPathOf(entry) {
|
|
5062
|
+
return typeof entry === "string" ? entry : entry.path;
|
|
5063
|
+
}
|
|
5064
|
+
function dedupeSharedPaths(entries) {
|
|
5065
|
+
const byPath = new Map;
|
|
5066
|
+
for (const entry of entries)
|
|
5067
|
+
byPath.set(sharedPathOf(entry), entry);
|
|
5068
|
+
return [...byPath.values()];
|
|
5069
|
+
}
|
|
5070
|
+
function resolveSharedPath(paths, entry) {
|
|
5071
|
+
if (typeof entry === "string")
|
|
5072
|
+
return { path: entry, target: `${paths.shared}/${entry}`, seed: true };
|
|
5073
|
+
return {
|
|
5074
|
+
path: entry.path,
|
|
5075
|
+
target: entry.target ?? `${paths.shared}/${entry.path}`,
|
|
5076
|
+
seed: entry.seed !== false
|
|
5077
|
+
};
|
|
5078
|
+
}
|
|
5079
|
+
function buildAdoptSharedPathFn(paths) {
|
|
5080
|
+
return [
|
|
5081
|
+
"ts_cloud_adopt_shared() {",
|
|
5082
|
+
' TS_CLOUD_SP="$1"',
|
|
5083
|
+
' TS_CLOUD_DST="$2"',
|
|
5084
|
+
` TS_CLOUD_SRC="${paths.current}/$TS_CLOUD_SP"`,
|
|
5085
|
+
' if [ -e "$TS_CLOUD_DST" ]; then return 0; fi',
|
|
5086
|
+
' if [ -L "$TS_CLOUD_SRC" ] || [ ! -e "$TS_CLOUD_SRC" ]; then return 0; fi',
|
|
5087
|
+
' mkdir -p "$(dirname "$TS_CLOUD_DST")"',
|
|
5088
|
+
' cp -a "$TS_CLOUD_SRC" "$TS_CLOUD_DST"',
|
|
5089
|
+
" for TS_CLOUD_SIDECAR in -wal -shm; do",
|
|
5090
|
+
' if [ -f "$TS_CLOUD_SRC$TS_CLOUD_SIDECAR" ]; then cp -a "$TS_CLOUD_SRC$TS_CLOUD_SIDECAR" "$TS_CLOUD_DST$TS_CLOUD_SIDECAR"; fi',
|
|
5091
|
+
" done",
|
|
5092
|
+
' echo "[ts-cloud] adopted $TS_CLOUD_SP from the live release into shared/ — it now survives deploys"',
|
|
5093
|
+
" return 0",
|
|
5094
|
+
"}"
|
|
5095
|
+
];
|
|
5096
|
+
}
|
|
4709
5097
|
function buildEnsureReleaseLayout(paths, sharedPaths = DEFAULT_SHARED_PATHS) {
|
|
4710
|
-
const lines = [`mkdir -p ${paths.releases} ${paths.shared}
|
|
4711
|
-
|
|
5098
|
+
const lines = [`mkdir -p ${paths.releases} ${paths.shared}`, ...buildAdoptSharedPathFn(paths)];
|
|
5099
|
+
const resolved = sharedPaths.map((entry) => resolveSharedPath(paths, entry));
|
|
5100
|
+
for (const { path: p, target, seed } of resolved) {
|
|
5101
|
+
if (!seed)
|
|
5102
|
+
continue;
|
|
5103
|
+
lines.push(`ts_cloud_adopt_shared '${p}' '${target}'`);
|
|
4712
5104
|
if (isFileSharedPath(p)) {
|
|
4713
|
-
lines.push(`mkdir -p "$(dirname ${
|
|
5105
|
+
lines.push(`mkdir -p "$(dirname ${target})"`, `touch ${target}`);
|
|
4714
5106
|
} else if (p === "storage") {
|
|
4715
|
-
lines.push(`mkdir -p ${
|
|
5107
|
+
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
5108
|
} else {
|
|
4717
|
-
lines.push(`mkdir -p ${
|
|
5109
|
+
lines.push(`mkdir -p ${target}`);
|
|
4718
5110
|
}
|
|
4719
5111
|
}
|
|
5112
|
+
const manifest = sharedPathsManifestPath(paths.base);
|
|
5113
|
+
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
5114
|
return lines;
|
|
4721
5115
|
}
|
|
4722
5116
|
function buildDeployLock(paths, waitSeconds = 900) {
|
|
@@ -4769,25 +5163,47 @@ function buildPromoteStagedRelease(paths) {
|
|
|
4769
5163
|
}
|
|
4770
5164
|
function buildLinkSharedPaths(paths, sharedPaths = DEFAULT_SHARED_PATHS) {
|
|
4771
5165
|
const lines = [];
|
|
4772
|
-
for (const
|
|
4773
|
-
const target =
|
|
5166
|
+
for (const entry of sharedPaths) {
|
|
5167
|
+
const { path: p, target, seed } = resolveSharedPath(paths, entry);
|
|
4774
5168
|
const link = `${paths.release}/${p}`;
|
|
4775
|
-
|
|
5169
|
+
const relink = [`rm -rf ${link}`, `mkdir -p "$(dirname ${link})"`, `ln -sfn ${target} ${link}`];
|
|
5170
|
+
if (seed) {
|
|
5171
|
+
lines.push(...relink);
|
|
5172
|
+
continue;
|
|
5173
|
+
}
|
|
5174
|
+
lines.push(`if [ -e ${target} ]; then`, ...relink.map((l) => ` ${l}`), "fi");
|
|
4776
5175
|
}
|
|
4777
5176
|
return lines;
|
|
4778
5177
|
}
|
|
5178
|
+
function buildRelinkSharedPaths(paths, releaseExpr) {
|
|
5179
|
+
const manifest = sharedPathsManifestPath(paths.base);
|
|
5180
|
+
return [
|
|
5181
|
+
`if [ -f ${manifest} ]; then`,
|
|
5182
|
+
' while IFS="\t" read -r TS_CLOUD_SP TS_CLOUD_TGT; do',
|
|
5183
|
+
' [ -n "$TS_CLOUD_SP" ] || continue',
|
|
5184
|
+
` [ -n "$TS_CLOUD_TGT" ] || TS_CLOUD_TGT="${paths.shared}/$TS_CLOUD_SP"`,
|
|
5185
|
+
' [ -e "$TS_CLOUD_TGT" ] || continue',
|
|
5186
|
+
` rm -rf ${releaseExpr}/"$TS_CLOUD_SP"`,
|
|
5187
|
+
` mkdir -p "$(dirname ${releaseExpr}/"$TS_CLOUD_SP")"`,
|
|
5188
|
+
` ln -sfn "$TS_CLOUD_TGT" ${releaseExpr}/"$TS_CLOUD_SP"`,
|
|
5189
|
+
` done < ${manifest}`,
|
|
5190
|
+
"fi"
|
|
5191
|
+
];
|
|
5192
|
+
}
|
|
4779
5193
|
function buildActivateRelease(paths) {
|
|
4780
5194
|
return [`ln -sfn ${paths.release} ${paths.current}.tmp`, `mv -Tf ${paths.current}.tmp ${paths.current}`];
|
|
4781
5195
|
}
|
|
4782
5196
|
function buildRollbackScript(paths, options = {}) {
|
|
4783
5197
|
const flip = options.to ? [
|
|
4784
5198
|
`[ -d ${paths.releases}/${options.to} ] || { echo "rollback target ${paths.releases}/${options.to} not found" >&2; exit 1; }`,
|
|
5199
|
+
...buildRelinkSharedPaths(paths, `${paths.releases}/${options.to}`),
|
|
4785
5200
|
`ln -sfn ${paths.releases}/${options.to} ${paths.current}.tmp`,
|
|
4786
5201
|
`mv -Tf ${paths.current}.tmp ${paths.current}`
|
|
4787
5202
|
] : [
|
|
4788
5203
|
`TS_CLOUD_CURRENT=$(readlink -f ${paths.current} 2>/dev/null || true)`,
|
|
4789
5204
|
`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
5205
|
'[ -n "$TS_CLOUD_PREV" ] || { echo "no previous release to roll back to" >&2; exit 1; }',
|
|
5206
|
+
...buildRelinkSharedPaths(paths, '"$TS_CLOUD_PREV"'),
|
|
4791
5207
|
`ln -sfn "$TS_CLOUD_PREV" ${paths.current}.tmp`,
|
|
4792
5208
|
`mv -Tf ${paths.current}.tmp ${paths.current}`,
|
|
4793
5209
|
'echo "rolled back to $TS_CLOUD_PREV"'
|
|
@@ -4875,7 +5291,7 @@ function buildSiteDeployScript(options) {
|
|
|
4875
5291
|
const unitBase = `${slug}-${siteName}`;
|
|
4876
5292
|
const serviceName = `${unitBase}.service`;
|
|
4877
5293
|
const tarball = releaseTarballTmpPath(slug, siteName, releaseId);
|
|
4878
|
-
const sharedPaths =
|
|
5294
|
+
const sharedPaths = dedupeSharedPaths([".env", ...options.sharedPaths ?? []]);
|
|
4879
5295
|
const envFile = formatEnvFile(envEntries);
|
|
4880
5296
|
const preStart = preStartCommands.length > 0 ? [`cd ${paths.release}`, ...preStartCommands] : [];
|
|
4881
5297
|
const stageRelease = [
|
|
@@ -5353,7 +5769,7 @@ function queueUnitName(slug, siteName, index) {
|
|
|
5353
5769
|
function daemonUnitName(slug, siteName, daemon, index) {
|
|
5354
5770
|
return `${slug}-${siteName}-daemon-${daemon.name ? slugify(daemon.name) : slugify(daemon.command).slice(0, 32) || String(index)}`;
|
|
5355
5771
|
}
|
|
5356
|
-
function
|
|
5772
|
+
function systemdUnit2(opts) {
|
|
5357
5773
|
const lines = [
|
|
5358
5774
|
"[Unit]",
|
|
5359
5775
|
`Description=${opts.description}`,
|
|
@@ -5400,7 +5816,7 @@ function buildSiteServicesScript(options) {
|
|
|
5400
5816
|
for (let p = 0;p < processes; p++) {
|
|
5401
5817
|
const name = queueUnitName(slug, siteName, desiredUnits.length);
|
|
5402
5818
|
desiredUnits.push(name);
|
|
5403
|
-
out.push(...writeUnitScript(name,
|
|
5819
|
+
out.push(...writeUnitScript(name, systemdUnit2({
|
|
5404
5820
|
description: `${siteName} queue worker ${qIndex}.${p} (managed by ts-cloud)`,
|
|
5405
5821
|
workingDir: current,
|
|
5406
5822
|
execStart: driver.wrapExec(driver.queueWorkerCommand(worker, ctx)),
|
|
@@ -5415,7 +5831,7 @@ function buildSiteServicesScript(options) {
|
|
|
5415
5831
|
for (let p = 0;p < processes; p++) {
|
|
5416
5832
|
const name = `${daemonUnitName(slug, siteName, daemon, dIndex)}-${p}`;
|
|
5417
5833
|
desiredUnits.push(name);
|
|
5418
|
-
out.push(...writeUnitScript(name,
|
|
5834
|
+
out.push(...writeUnitScript(name, systemdUnit2({
|
|
5419
5835
|
description: `${siteName} daemon ${daemon.name || daemon.command} (managed by ts-cloud)`,
|
|
5420
5836
|
workingDir: daemon.directory || current,
|
|
5421
5837
|
execStart: driver.wrapExec(daemon.command),
|
|
@@ -5430,7 +5846,7 @@ function buildSiteServicesScript(options) {
|
|
|
5430
5846
|
const schedulerUnit = `${slug}-${siteName}-scheduler`;
|
|
5431
5847
|
if (schedulerEnabled && driver.schedulerMode === "daemon") {
|
|
5432
5848
|
desiredUnits.push(schedulerUnit);
|
|
5433
|
-
out.push(...writeUnitScript(schedulerUnit,
|
|
5849
|
+
out.push(...writeUnitScript(schedulerUnit, systemdUnit2({
|
|
5434
5850
|
description: `${siteName} scheduler (managed by ts-cloud)`,
|
|
5435
5851
|
workingDir: current,
|
|
5436
5852
|
execStart: driver.wrapExec(driver.schedulerCommand(ctx)),
|