@stacksjs/ts-cloud 0.7.29 → 0.7.31
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 +774 -767
- package/dist/{chunk-wv2ch2yb.js → chunk-0c69nbaa.js} +253 -101
- package/dist/{chunk-nrkrr2yw.js → chunk-crcjdm09.js} +8 -7
- package/dist/{chunk-4ew46pj6.js → chunk-ebx9gjnn.js} +2 -2
- package/dist/{chunk-k56wpkhg.js → chunk-stt1z5cx.js} +4 -1
- package/dist/{chunk-3mcfbrw8.js → chunk-tskj9fay.js} +1 -1
- package/dist/deploy/index.js +4 -4
- package/dist/drivers/hetzner/client.d.ts +8 -0
- package/dist/drivers/hetzner/driver.d.ts +16 -0
- package/dist/drivers/index.d.ts +3 -3
- package/dist/drivers/index.js +6 -2
- package/dist/drivers/local-box/driver.d.ts +1 -1
- package/dist/drivers/shared/compute-deploy.d.ts +9 -3
- package/dist/drivers/shared/deploy-script.d.ts +12 -2
- package/dist/drivers/shared/fleet.d.ts +12 -1
- package/dist/drivers/shared/rpx-gateway.d.ts +26 -0
- package/dist/drivers/shared/ssh-keys.d.ts +3 -2
- package/dist/index.js +6 -4
- package/dist/ui/index.html +3 -3
- package/dist/ui/server/actions.html +3 -3
- package/dist/ui/server/backups.html +3 -3
- package/dist/ui/server/database.html +3 -3
- package/dist/ui/server/deployments.html +3 -3
- package/dist/ui/server/firewall.html +3 -3
- package/dist/ui/server/logs.html +3 -3
- package/dist/ui/server/services.html +3 -3
- package/dist/ui/server/sites.html +3 -3
- package/dist/ui/server/ssh-keys.html +3 -3
- package/dist/ui/server/team.html +3 -3
- package/dist/ui/server/terminal.html +3 -3
- package/dist/ui/server/workers.html +3 -3
- package/dist/ui/serverless/alarms.html +3 -3
- package/dist/ui/serverless/assets.html +3 -3
- package/dist/ui/serverless/data.html +3 -3
- package/dist/ui/serverless/deployments.html +3 -3
- package/dist/ui/serverless/functions.html +3 -3
- package/dist/ui/serverless/logs.html +3 -3
- package/dist/ui/serverless/queues.html +3 -3
- package/dist/ui/serverless/scheduler.html +3 -3
- package/dist/ui/serverless/secrets.html +3 -3
- package/dist/ui/serverless/traces.html +3 -3
- package/dist/ui/serverless.html +3 -3
- package/package.json +3 -3
|
@@ -2,10 +2,11 @@ import {
|
|
|
2
2
|
deploymentCoexistenceError,
|
|
3
3
|
hasManagementDashboardSite,
|
|
4
4
|
isManagementDashboardSiteName,
|
|
5
|
+
resolveAppDatabase,
|
|
5
6
|
resolveCloudProvider,
|
|
6
7
|
resolveManagementDashboardSites,
|
|
7
8
|
resolveProjectStackName
|
|
8
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-stt1z5cx.js";
|
|
9
10
|
import {
|
|
10
11
|
EC2Client,
|
|
11
12
|
SSMClient
|
|
@@ -145,7 +146,7 @@ function buildDatabaseSetupScript(database, services = {}) {
|
|
|
145
146
|
const lines = [];
|
|
146
147
|
for (const db of dbs) {
|
|
147
148
|
if (u.access === "readonly") {
|
|
148
|
-
lines.push(`GRANT CONNECT ON DATABASE ${pgIdent(db)} TO ${pgIdent(u.username)};`, `\\connect ${pgIdent(db)}`, `GRANT USAGE ON SCHEMA public TO ${pgIdent(u.username)};`, `GRANT SELECT ON ALL TABLES IN SCHEMA public TO ${pgIdent(u.username)};`, `ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO ${pgIdent(u.username)};`, "\\connect postgres");
|
|
149
|
+
lines.push(`GRANT CONNECT ON DATABASE ${pgIdent(db)} TO ${pgIdent(u.username)};`, `\\connect ${pgIdent(db)}`, `GRANT USAGE ON SCHEMA public TO ${pgIdent(u.username)};`, `GRANT SELECT ON ALL TABLES IN SCHEMA public TO ${pgIdent(u.username)};`, `ALTER DEFAULT PRIVILEGES FOR ROLE ${pgIdent(user)} IN SCHEMA public GRANT SELECT ON TABLES TO ${pgIdent(u.username)};`, "\\connect postgres");
|
|
149
150
|
} else {
|
|
150
151
|
lines.push(`GRANT ALL PRIVILEGES ON DATABASE ${pgIdent(db)} TO ${pgIdent(u.username)};`);
|
|
151
152
|
}
|
|
@@ -810,7 +811,9 @@ import { startProxies } from '@stacksjs/rpx'
|
|
|
810
811
|
|
|
811
812
|
const config = ${json} as const
|
|
812
813
|
|
|
813
|
-
|
|
814
|
+
// Verbose is the hard default for ts-cloud-installed gateways (RPX_VERBOSE=false
|
|
815
|
+
// opts out) — without it rpx's TLS/routing diagnostics never reach the journal.
|
|
816
|
+
await startProxies({ verbose: process.env.RPX_VERBOSE !== 'false', ...config } as any)
|
|
814
817
|
`;
|
|
815
818
|
}
|
|
816
819
|
function usesRpxProxy(compute) {
|
|
@@ -869,6 +872,12 @@ const config = {
|
|
|
869
872
|
https: true,
|
|
870
873
|
hostsManagement: false,
|
|
871
874
|
cleanup: { hosts: false, certs: false },
|
|
875
|
+
// Verbose is the hard default for ts-cloud-installed gateways: without it,
|
|
876
|
+
// rpx's TLS/routing diagnostics (tlsx on-demand 'refused issuance',
|
|
877
|
+
// 'issuance failed', 'adopted existing on-disk cert') never reach the systemd
|
|
878
|
+
// journal and a production TLS failure looks like "nothing happens". Silence
|
|
879
|
+
// it by setting RPX_VERBOSE=false on the systemd unit.
|
|
880
|
+
verbose: process.env.RPX_VERBOSE !== 'false',
|
|
872
881
|
...(suffixes.size > 0 ? { onDemandTls: { enabled: true, allowedSuffixes: [...suffixes], email, certsDir } } : {}),
|
|
873
882
|
...(acmeChallengeWebroot ? { acmeChallengeWebroot } : {}),
|
|
874
883
|
...(guard ? { originGuard: { header: guard.header, value: guard.value, hosts: [...guardHosts] } } : {}),
|
|
@@ -877,14 +886,14 @@ const config = {
|
|
|
877
886
|
await startProxies(config)
|
|
878
887
|
`;
|
|
879
888
|
}
|
|
880
|
-
function writeFileHeredoc(path, content, delimiter) {
|
|
889
|
+
function writeFileHeredoc(path, content, delimiter, mode = "0644") {
|
|
881
890
|
return [
|
|
882
891
|
`__tsc_tmp="$(mktemp "${path}.XXXXXX")"`,
|
|
883
892
|
`cat > "$__tsc_tmp" <<'${delimiter}'`,
|
|
884
893
|
content,
|
|
885
894
|
delimiter,
|
|
886
895
|
`mv -f "$__tsc_tmp" ${path}`,
|
|
887
|
-
`chmod
|
|
896
|
+
`chmod ${mode} ${path}`
|
|
888
897
|
];
|
|
889
898
|
}
|
|
890
899
|
function certDomainsForConfig(config) {
|
|
@@ -986,10 +995,14 @@ function buildRpxProvisionScript(options) {
|
|
|
986
995
|
return [
|
|
987
996
|
"set -euo pipefail",
|
|
988
997
|
`mkdir -p ${RPX_DIR} ${RPX_SITES_DIR} ${certsDir} ${RPX_INSTALL_DIR}`,
|
|
989
|
-
`rm -rf ${RPX_INSTALL_DIR}
|
|
990
|
-
`
|
|
998
|
+
`rm -rf ${RPX_INSTALL_DIR}.next ${RPX_INSTALL_DIR}.prev`,
|
|
999
|
+
`mkdir -p ${RPX_INSTALL_DIR}.next`,
|
|
1000
|
+
`(cd ${RPX_INSTALL_DIR}.next && ${bunBin} add @stacksjs/rpx@${version})`,
|
|
1001
|
+
`mv ${RPX_INSTALL_DIR} ${RPX_INSTALL_DIR}.prev`,
|
|
1002
|
+
`mv ${RPX_INSTALL_DIR}.next ${RPX_INSTALL_DIR}`,
|
|
1003
|
+
`rm -rf ${RPX_INSTALL_DIR}.prev`,
|
|
991
1004
|
`ln -sfn ${RPX_INSTALL_DIR}/node_modules ${RPX_DIR}/node_modules`,
|
|
992
|
-
...writeFileHeredoc(`${RPX_SITES_DIR}/${slug}.json`, fragment, "TS_CLOUD_RPX_FRAGMENT_EOF"),
|
|
1005
|
+
...writeFileHeredoc(`${RPX_SITES_DIR}/${slug}.json`, fragment, "TS_CLOUD_RPX_FRAGMENT_EOF", "0600"),
|
|
993
1006
|
...writeFileHeredoc(RPX_LAUNCHER_PATH, assembler, "TS_CLOUD_RPX_EOF"),
|
|
994
1007
|
...writeFileHeredoc(`/etc/systemd/system/${RPX_SERVICE_NAME}`, [
|
|
995
1008
|
"[Unit]",
|
|
@@ -1020,13 +1033,23 @@ function buildRpxProvisionScript(options) {
|
|
|
1020
1033
|
...buildCertManagementCommands(options)
|
|
1021
1034
|
];
|
|
1022
1035
|
}
|
|
1036
|
+
function buildRpxFragmentRefreshScript(options) {
|
|
1037
|
+
const slug = (options.slug || "app").replace(/[^a-z0-9._-]+/gi, "-");
|
|
1038
|
+
const fragment = JSON.stringify({ slug, ...options.config }, null, 2);
|
|
1039
|
+
return [
|
|
1040
|
+
"set -euo pipefail",
|
|
1041
|
+
`mkdir -p ${RPX_SITES_DIR}`,
|
|
1042
|
+
...writeFileHeredoc(`${RPX_SITES_DIR}/${slug}.json`, fragment, "TS_CLOUD_RPX_FRAGMENT_EOF", "0600"),
|
|
1043
|
+
`systemctl restart ${RPX_SERVICE_NAME}`
|
|
1044
|
+
];
|
|
1045
|
+
}
|
|
1023
1046
|
|
|
1024
1047
|
// src/drivers/shared/ufw.ts
|
|
1025
1048
|
var UFW_BASE_PORTS = [80, 443];
|
|
1026
1049
|
function buildUfwScript(firewall = {}) {
|
|
1027
1050
|
if (firewall.enabled === false)
|
|
1028
1051
|
return [];
|
|
1029
|
-
const ports = [...new Set([...UFW_BASE_PORTS, ...firewall.allowedPorts || []])].filter((p) => p
|
|
1052
|
+
const ports = [...new Set([...UFW_BASE_PORTS, ...firewall.allowedPorts || []])].filter((p) => Number.isInteger(p) && p >= 1 && p <= 65535).sort((a, b) => a - b);
|
|
1030
1053
|
const lines = [
|
|
1031
1054
|
"export DEBIAN_FRONTEND=noninteractive",
|
|
1032
1055
|
"apt-get install -y ufw",
|
|
@@ -1157,25 +1180,23 @@ var BLOCK_BEGIN = "# >>> ts-cloud managed keys >>>";
|
|
|
1157
1180
|
var BLOCK_END = "# <<< ts-cloud managed keys <<<";
|
|
1158
1181
|
var DEFAULT_AUTHORIZED_KEYS = "/root/.ssh/authorized_keys";
|
|
1159
1182
|
function buildAuthorizedKeysScript(keys = [], options = {}) {
|
|
1160
|
-
if (keys.length === 0)
|
|
1161
|
-
return [];
|
|
1162
1183
|
const path = options.path ?? DEFAULT_AUTHORIZED_KEYS;
|
|
1163
1184
|
const dir = path.replace(/\/[^/]*$/, "");
|
|
1164
|
-
const
|
|
1165
|
-
BLOCK_BEGIN,
|
|
1166
|
-
...keys.map((k) => `${k.publicKey.trim()} ${k.name}`),
|
|
1167
|
-
BLOCK_END
|
|
1168
|
-
].join(`
|
|
1169
|
-
`);
|
|
1170
|
-
return [
|
|
1185
|
+
const lines = [
|
|
1171
1186
|
`mkdir -p ${dir}`,
|
|
1172
1187
|
`touch ${path}`,
|
|
1173
|
-
`sed -i '/^${escapeSed(BLOCK_BEGIN)}$/,/^${escapeSed(BLOCK_END)}$/d' ${path}
|
|
1174
|
-
`cat >> ${path} <<'TS_CLOUD_KEYS_EOF'`,
|
|
1175
|
-
block,
|
|
1176
|
-
"TS_CLOUD_KEYS_EOF",
|
|
1177
|
-
`chmod 600 ${path}`
|
|
1188
|
+
`sed -i '/^${escapeSed(BLOCK_BEGIN)}$/,/^${escapeSed(BLOCK_END)}$/d' ${path}`
|
|
1178
1189
|
];
|
|
1190
|
+
if (keys.length > 0) {
|
|
1191
|
+
const block = [
|
|
1192
|
+
BLOCK_BEGIN,
|
|
1193
|
+
...keys.map((k) => `${k.publicKey.trim()} ${k.name}`),
|
|
1194
|
+
BLOCK_END
|
|
1195
|
+
].join(`
|
|
1196
|
+
`);
|
|
1197
|
+
lines.push(`cat >> ${path} <<'TS_CLOUD_KEYS_EOF'`, block, "TS_CLOUD_KEYS_EOF", `chmod 600 ${path}`);
|
|
1198
|
+
}
|
|
1199
|
+
return lines;
|
|
1179
1200
|
}
|
|
1180
1201
|
function escapeSed(value) {
|
|
1181
1202
|
return value.replace(/[.*[\]\\/^$]/g, "\\$&");
|
|
@@ -1399,11 +1420,12 @@ function buildComputeProvisionScripts(config) {
|
|
|
1399
1420
|
...useNginx ? buildNginxServiceScript() : []
|
|
1400
1421
|
] : undefined;
|
|
1401
1422
|
const extras = [];
|
|
1423
|
+
const appDatabase = resolveAppDatabase(config);
|
|
1402
1424
|
if (!phpBox && needsPantry)
|
|
1403
1425
|
extras.push(...pantryBootstrap);
|
|
1404
1426
|
extras.push(...buildNotifierScript(config.notifications));
|
|
1405
1427
|
if (compute.managedServices) {
|
|
1406
|
-
extras.push(...buildServicesProvisionScript(compute.managedServices), ...buildDatabaseSetupScript(
|
|
1428
|
+
extras.push(...buildServicesProvisionScript(compute.managedServices), ...buildDatabaseSetupScript(appDatabase, compute.managedServices));
|
|
1407
1429
|
}
|
|
1408
1430
|
extras.push(...buildUfwScript(compute.firewall ?? (phpBox ? { enabled: true } : { enabled: false })));
|
|
1409
1431
|
extras.push(...buildAutoUpdatesScript(compute.autoUpdates ?? phpBox));
|
|
@@ -1411,7 +1433,7 @@ function buildComputeProvisionScripts(config) {
|
|
|
1411
1433
|
extras.push(...buildAuthorizedKeysScript(compute.sshKeys));
|
|
1412
1434
|
if (compute.backups?.enabled) {
|
|
1413
1435
|
extras.push(...buildBackupProvisionScript({
|
|
1414
|
-
database:
|
|
1436
|
+
database: appDatabase,
|
|
1415
1437
|
backups: compute.backups
|
|
1416
1438
|
}));
|
|
1417
1439
|
}
|
|
@@ -1514,7 +1536,6 @@ ln -sf /root/.deno/bin/deno /usr/local/bin/deno
|
|
|
1514
1536
|
mkdir -p /var/www /var/ts-cloud/staging /var/ts-cloud/releases
|
|
1515
1537
|
`;
|
|
1516
1538
|
if (caddyfile) {
|
|
1517
|
-
const escaped = caddyfile.replace(/\$/g, "\\$");
|
|
1518
1539
|
script += `
|
|
1519
1540
|
ARCH=$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/')
|
|
1520
1541
|
curl -fsSL "https://caddyserver.com/api/download?os=linux&arch=\${ARCH}" -o /usr/local/bin/caddy
|
|
@@ -1552,7 +1573,7 @@ WantedBy=multi-user.target
|
|
|
1552
1573
|
CADDY_UNIT_EOF
|
|
1553
1574
|
|
|
1554
1575
|
cat > /etc/caddy/Caddyfile <<'CADDY_CONFIG_EOF'
|
|
1555
|
-
${
|
|
1576
|
+
${caddyfile}
|
|
1556
1577
|
CADDY_CONFIG_EOF
|
|
1557
1578
|
|
|
1558
1579
|
systemctl daemon-reload
|
|
@@ -1858,7 +1879,7 @@ class AwsDriver {
|
|
|
1858
1879
|
if (!sendResult.CommandId) {
|
|
1859
1880
|
return { success: false, instanceCount: 0, perInstance: [], error: "Failed to send SSM command" };
|
|
1860
1881
|
}
|
|
1861
|
-
return this.pollSsmCommand(ssm, sendResult.CommandId, options.targets.length);
|
|
1882
|
+
return this.pollSsmCommand(ssm, sendResult.CommandId, options.targets.length, ((options.timeoutSeconds || 600) + 30) * 1000);
|
|
1862
1883
|
}
|
|
1863
1884
|
if (!options.tags || Object.keys(options.tags).length === 0) {
|
|
1864
1885
|
return { success: false, instanceCount: 0, perInstance: [], error: "No targets or tags provided for AWS deploy" };
|
|
@@ -1881,9 +1902,8 @@ class AwsDriver {
|
|
|
1881
1902
|
error: result.error
|
|
1882
1903
|
};
|
|
1883
1904
|
}
|
|
1884
|
-
async pollSsmCommand(ssm, commandId, expectedCount) {
|
|
1905
|
+
async pollSsmCommand(ssm, commandId, expectedCount, maxWait = 600000) {
|
|
1885
1906
|
const pollInterval = 3000;
|
|
1886
|
-
const maxWait = 600000;
|
|
1887
1907
|
const startTime = Date.now();
|
|
1888
1908
|
const terminalStatuses = new Set(["Success", "Failed", "Cancelled", "TimedOut"]);
|
|
1889
1909
|
let lastInvocations = [];
|
|
@@ -2019,9 +2039,21 @@ class HetznerClient {
|
|
|
2019
2039
|
}
|
|
2020
2040
|
return data;
|
|
2021
2041
|
}
|
|
2042
|
+
async requestAll(path, key) {
|
|
2043
|
+
const items = [];
|
|
2044
|
+
let page = 1;
|
|
2045
|
+
for (;; ) {
|
|
2046
|
+
const sep = path.includes("?") ? "&" : "?";
|
|
2047
|
+
const data = await this.request("GET", `${path}${sep}per_page=50&page=${page}`);
|
|
2048
|
+
items.push(...data[key] ?? []);
|
|
2049
|
+
const next = data.meta?.pagination?.next_page;
|
|
2050
|
+
if (!next)
|
|
2051
|
+
return items;
|
|
2052
|
+
page = next;
|
|
2053
|
+
}
|
|
2054
|
+
}
|
|
2022
2055
|
async listServers() {
|
|
2023
|
-
|
|
2024
|
-
return data.servers;
|
|
2056
|
+
return this.requestAll("/servers", "servers");
|
|
2025
2057
|
}
|
|
2026
2058
|
async getServer(id) {
|
|
2027
2059
|
const data = await this.request("GET", `/servers/${id}`);
|
|
@@ -2044,8 +2076,7 @@ class HetznerClient {
|
|
|
2044
2076
|
return { server: data.server, action: data.action };
|
|
2045
2077
|
}
|
|
2046
2078
|
async listNetworks() {
|
|
2047
|
-
|
|
2048
|
-
return data.networks;
|
|
2079
|
+
return this.requestAll("/networks", "networks");
|
|
2049
2080
|
}
|
|
2050
2081
|
async createNetwork(options) {
|
|
2051
2082
|
const ipRange = options.ipRange ?? "10.0.0.0/16";
|
|
@@ -2061,8 +2092,7 @@ class HetznerClient {
|
|
|
2061
2092
|
await this.request("DELETE", `/networks/${id}`);
|
|
2062
2093
|
}
|
|
2063
2094
|
async listLoadBalancers() {
|
|
2064
|
-
|
|
2065
|
-
return data.load_balancers;
|
|
2095
|
+
return this.requestAll("/load_balancers", "load_balancers");
|
|
2066
2096
|
}
|
|
2067
2097
|
async createLoadBalancer(options) {
|
|
2068
2098
|
const data = await this.request("POST", "/load_balancers", {
|
|
@@ -2097,8 +2127,7 @@ class HetznerClient {
|
|
|
2097
2127
|
return data.action;
|
|
2098
2128
|
}
|
|
2099
2129
|
async listFirewalls() {
|
|
2100
|
-
|
|
2101
|
-
return data.firewalls;
|
|
2130
|
+
return this.requestAll("/firewalls", "firewalls");
|
|
2102
2131
|
}
|
|
2103
2132
|
async createFirewall(options) {
|
|
2104
2133
|
const data = await this.request("POST", "/firewalls", {
|
|
@@ -2125,8 +2154,7 @@ class HetznerClient {
|
|
|
2125
2154
|
return data.actions;
|
|
2126
2155
|
}
|
|
2127
2156
|
async listSshKeys() {
|
|
2128
|
-
|
|
2129
|
-
return data.ssh_keys;
|
|
2157
|
+
return this.requestAll("/ssh_keys", "ssh_keys");
|
|
2130
2158
|
}
|
|
2131
2159
|
async createSshKey(options) {
|
|
2132
2160
|
const data = await this.request("POST", "/ssh_keys", {
|
|
@@ -2223,6 +2251,19 @@ function buildFleetServicesEnv(servicesPrivateIp, database) {
|
|
|
2223
2251
|
}
|
|
2224
2252
|
return env2;
|
|
2225
2253
|
}
|
|
2254
|
+
function buildFleetServicesBoxProvision(config) {
|
|
2255
|
+
const compute = config.infrastructure?.compute ?? {};
|
|
2256
|
+
const appDatabase = resolveAppDatabase(config);
|
|
2257
|
+
return [
|
|
2258
|
+
...buildServicesProvisionScript(compute.managedServices ?? { mysql: true, redis: true }, { bindPrivate: true }),
|
|
2259
|
+
...buildDatabaseSetupScript(appDatabase, compute.managedServices ?? { mysql: true }),
|
|
2260
|
+
...buildAutoUpdatesScript(true),
|
|
2261
|
+
...buildMonitoringScript(true),
|
|
2262
|
+
...buildAuthorizedKeysScript(compute.sshKeys),
|
|
2263
|
+
...buildNotifierScript(config.notifications),
|
|
2264
|
+
...compute.backups?.enabled ? [...buildBackupProvisionScript({ database: appDatabase, backups: compute.backups })] : []
|
|
2265
|
+
];
|
|
2266
|
+
}
|
|
2226
2267
|
|
|
2227
2268
|
// src/drivers/hetzner/firewall-rules.ts
|
|
2228
2269
|
function buildHetznerFirewallRules(config) {
|
|
@@ -2274,7 +2315,7 @@ function matchesTsCloudLabels(labels, slug, environment, role = "app") {
|
|
|
2274
2315
|
}
|
|
2275
2316
|
|
|
2276
2317
|
// src/drivers/hetzner/state.ts
|
|
2277
|
-
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2318
|
+
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
2278
2319
|
import { join as join3 } from "node:path";
|
|
2279
2320
|
var STATE_DIR = "storage/cloud/state";
|
|
2280
2321
|
function driverStatePath(stackName) {
|
|
@@ -2291,8 +2332,10 @@ async function readDriverState(stackName) {
|
|
|
2291
2332
|
async function writeDriverState(stackName, state) {
|
|
2292
2333
|
const path = driverStatePath(stackName);
|
|
2293
2334
|
await mkdir(join3(process.cwd(), STATE_DIR), { recursive: true });
|
|
2294
|
-
|
|
2335
|
+
const tmp = `${path}.${process.pid}.tmp`;
|
|
2336
|
+
await writeFile(tmp, `${JSON.stringify(state, null, 2)}
|
|
2295
2337
|
`, "utf8");
|
|
2338
|
+
await rename(tmp, path);
|
|
2296
2339
|
}
|
|
2297
2340
|
|
|
2298
2341
|
// src/drivers/hetzner/driver.ts
|
|
@@ -2300,7 +2343,7 @@ var SSH_MAX_BUFFER = 1024 * 1024 * 256;
|
|
|
2300
2343
|
var SSH_ERROR_OUTPUT_LIMIT = 8000;
|
|
2301
2344
|
function sshErrorOutput(value) {
|
|
2302
2345
|
const output = Buffer.isBuffer(value) ? value.toString("utf8") : typeof value === "string" ? value : "";
|
|
2303
|
-
return output.replace(/(^|\n)([A-
|
|
2346
|
+
return output.replace(/(^|\n)(\s*(?:export\s+)?[A-Za-z_][A-Za-z0-9_]*=).*$/gm, "$1$2[redacted]").replace(/encrypted:[A-Za-z0-9+/=]+/g, "encrypted:[redacted]").trim().slice(-SSH_ERROR_OUTPUT_LIMIT);
|
|
2304
2347
|
}
|
|
2305
2348
|
function formatSshFailure(error) {
|
|
2306
2349
|
const childError = error;
|
|
@@ -2358,6 +2401,15 @@ class HetznerDriver {
|
|
|
2358
2401
|
if (topology.dedicatedServices || topology.appServers > 1) {
|
|
2359
2402
|
return phpBox ? this.provisionFleet(options, topology) : this.provisionBunFleet(options, topology);
|
|
2360
2403
|
}
|
|
2404
|
+
const labels = tsCloudLabels(slug, environment, "app");
|
|
2405
|
+
const sites = config.sites || {};
|
|
2406
|
+
const sitePorts = this.collectUpstreamPorts(sites);
|
|
2407
|
+
const firewallName = `${slug}-${environment}-app-fw`;
|
|
2408
|
+
const { firewall } = await this.ensureFirewall(firewallName, labels, buildHetznerFirewallRules({
|
|
2409
|
+
allowSsh: compute.allowSsh !== false,
|
|
2410
|
+
sitePorts
|
|
2411
|
+
}));
|
|
2412
|
+
const sshKeyId = await this.ensureSshKey(slug, environment, labels);
|
|
2361
2413
|
const existing = await readDriverState(stackName);
|
|
2362
2414
|
if (existing?.serverId) {
|
|
2363
2415
|
const server2 = await this.tryGetServer(existing.serverId);
|
|
@@ -2365,7 +2417,6 @@ class HetznerDriver {
|
|
|
2365
2417
|
return this.outputsFromState(existing, server2);
|
|
2366
2418
|
}
|
|
2367
2419
|
}
|
|
2368
|
-
const labels = tsCloudLabels(slug, environment, "app");
|
|
2369
2420
|
const alreadyRunning = await this.findExistingServer(slug, environment, serverName);
|
|
2370
2421
|
if (alreadyRunning && alreadyRunning.status !== "off") {
|
|
2371
2422
|
const rehydrated = {
|
|
@@ -2380,8 +2431,6 @@ class HetznerDriver {
|
|
|
2380
2431
|
await writeDriverState(stackName, rehydrated);
|
|
2381
2432
|
return this.outputsFromState(rehydrated, alreadyRunning);
|
|
2382
2433
|
}
|
|
2383
|
-
const sites = config.sites || {};
|
|
2384
|
-
const sitePorts = this.collectUpstreamPorts(sites);
|
|
2385
2434
|
const rpxProvision = compute.proxy?.engine === "rpx" ? buildRpxProvisionScript({
|
|
2386
2435
|
proxy: compute.proxy,
|
|
2387
2436
|
config: buildRpxConfig(sites, { proxy: compute.proxy, slug: config.project.slug }),
|
|
@@ -2403,12 +2452,6 @@ class HetznerDriver {
|
|
|
2403
2452
|
const userData = wrapCloudInitUserData(bootstrap);
|
|
2404
2453
|
const serverType = resolveHetznerServerType(compute.size);
|
|
2405
2454
|
const image = resolveHetznerImage(config);
|
|
2406
|
-
const firewallName = `${slug}-${environment}-app-fw`;
|
|
2407
|
-
const { firewall } = await this.ensureFirewall(firewallName, labels, buildHetznerFirewallRules({
|
|
2408
|
-
allowSsh: compute.allowSsh !== false,
|
|
2409
|
-
sitePorts
|
|
2410
|
-
}));
|
|
2411
|
-
const sshKeyId = await this.ensureSshKey(slug, environment, labels);
|
|
2412
2455
|
const { server, action } = await this.client.createServer({
|
|
2413
2456
|
name: serverName,
|
|
2414
2457
|
serverType,
|
|
@@ -2474,13 +2517,7 @@ class HetznerDriver {
|
|
|
2474
2517
|
{ direction: "in", protocol: "tcp", port: "6379", source_ips: [network.ip_range] },
|
|
2475
2518
|
{ direction: "in", protocol: "tcp", port: "7700", source_ips: [network.ip_range] }
|
|
2476
2519
|
]);
|
|
2477
|
-
const servicesProvision =
|
|
2478
|
-
...buildServicesProvisionScript(compute.managedServices ?? { mysql: true, redis: true }, { bindPrivate: true }),
|
|
2479
|
-
...buildDatabaseSetupScript(config.infrastructure?.appDatabase, compute.managedServices ?? { mysql: true }),
|
|
2480
|
-
...buildAutoUpdatesScript(true),
|
|
2481
|
-
...buildMonitoringScript(true),
|
|
2482
|
-
...buildAuthorizedKeysScript(compute.sshKeys)
|
|
2483
|
-
];
|
|
2520
|
+
const servicesProvision = buildFleetServicesBoxProvision(config);
|
|
2484
2521
|
const servicesUserData = wrapCloudInitUserData(buildUbuntuBootstrapScript({ runtime: "php", servicesProvision, baked }));
|
|
2485
2522
|
const all = await this.client.listServers().catch(() => []);
|
|
2486
2523
|
const newServerIds = [];
|
|
@@ -2608,6 +2645,9 @@ class HetznerDriver {
|
|
|
2608
2645
|
if (existingState?.lbServerId) {
|
|
2609
2646
|
const lb = await this.tryGetServer(existingState.lbServerId);
|
|
2610
2647
|
if (lb && lb.status !== "off") {
|
|
2648
|
+
const rpxProxy = compute.proxy?.engine === "rpx" ? compute.proxy : { engine: "rpx" };
|
|
2649
|
+
const appTargets = await this.findComputeTargets({ slug, environment, role: "app", stackName });
|
|
2650
|
+
this.refreshLbRoutes(lb, sites, appTargets.map((t) => ({ privateIp: t.privateIp, publicIp: t.publicIp })), rpxProxy, slug);
|
|
2611
2651
|
return {
|
|
2612
2652
|
appPublicIp: lb.public_net.ipv4?.ip ?? existingState.publicIp,
|
|
2613
2653
|
loadBalancerIp: lb.public_net.ipv4?.ip ?? existingState.publicIp,
|
|
@@ -2638,7 +2678,8 @@ class HetznerDriver {
|
|
|
2638
2678
|
let servicesServerId;
|
|
2639
2679
|
let servicesPrivateIp;
|
|
2640
2680
|
const wantsServicesBox = !!compute.servicesServer || !!compute.managedServices;
|
|
2641
|
-
|
|
2681
|
+
const dedicatedServicesHere = topology.dedicatedServices && wantsServicesBox;
|
|
2682
|
+
if (dedicatedServicesHere) {
|
|
2642
2683
|
const { firewall: svcFw } = await this.ensureFirewall(`${slug}-${environment}-services-fw`, tsCloudLabels(slug, environment, "services"), [
|
|
2643
2684
|
{ direction: "in", protocol: "tcp", port: "22", source_ips: ["0.0.0.0/0", "::/0"] },
|
|
2644
2685
|
{ direction: "in", protocol: "tcp", port: "3306", source_ips: [network.ip_range] },
|
|
@@ -2646,13 +2687,7 @@ class HetznerDriver {
|
|
|
2646
2687
|
{ direction: "in", protocol: "tcp", port: "6379", source_ips: [network.ip_range] },
|
|
2647
2688
|
{ direction: "in", protocol: "tcp", port: "7700", source_ips: [network.ip_range] }
|
|
2648
2689
|
]);
|
|
2649
|
-
const servicesProvision =
|
|
2650
|
-
...buildServicesProvisionScript(compute.managedServices ?? { mysql: true, redis: true }, { bindPrivate: true }),
|
|
2651
|
-
...buildDatabaseSetupScript(config.infrastructure?.appDatabase, compute.managedServices ?? { mysql: true }),
|
|
2652
|
-
...buildAutoUpdatesScript(true),
|
|
2653
|
-
...buildMonitoringScript(true),
|
|
2654
|
-
...buildAuthorizedKeysScript(compute.sshKeys)
|
|
2655
|
-
];
|
|
2690
|
+
const servicesProvision = buildFleetServicesBoxProvision(config);
|
|
2656
2691
|
const servicesUserData = wrapCloudInitUserData(buildUbuntuBootstrapScript({ runtime: "bun", servicesProvision, baked }));
|
|
2657
2692
|
let svcServer = all.find((s) => matchesTsCloudLabels(s.labels, slug, environment, "services"));
|
|
2658
2693
|
if (!svcServer) {
|
|
@@ -2674,7 +2709,11 @@ class HetznerDriver {
|
|
|
2674
2709
|
servicesServerId = svcServer.id;
|
|
2675
2710
|
servicesPrivateIp = svcServer.private_net?.[0]?.ip ?? (await this.client.getServer(servicesServerId)).private_net?.[0]?.ip;
|
|
2676
2711
|
}
|
|
2677
|
-
const
|
|
2712
|
+
const appBoxCompute = dedicatedServicesHere ? { ...compute, managedServices: undefined, backups: undefined } : compute;
|
|
2713
|
+
const appProvisionScripts = buildComputeProvisionScripts({
|
|
2714
|
+
...config,
|
|
2715
|
+
infrastructure: { ...config.infrastructure, compute: appBoxCompute }
|
|
2716
|
+
});
|
|
2678
2717
|
const appUserData = wrapCloudInitUserData(buildUbuntuBootstrapScript({
|
|
2679
2718
|
runtime: appProvisionScripts.runtime,
|
|
2680
2719
|
runtimeVersion: appProvisionScripts.runtimeVersion,
|
|
@@ -2729,8 +2768,8 @@ class HetznerDriver {
|
|
|
2729
2768
|
let lbIp;
|
|
2730
2769
|
if (topology.loadBalancer) {
|
|
2731
2770
|
let lbServer = all.find((s) => matchesTsCloudLabels(s.labels, slug, environment, "lb"));
|
|
2771
|
+
const rpxProxy = compute.proxy?.engine === "rpx" ? compute.proxy : { engine: "rpx" };
|
|
2732
2772
|
if (!lbServer) {
|
|
2733
|
-
const rpxProxy = compute.proxy?.engine === "rpx" ? compute.proxy : { engine: "rpx" };
|
|
2734
2773
|
const lbRpxProvision = buildRpxProvisionScript({
|
|
2735
2774
|
proxy: rpxProxy,
|
|
2736
2775
|
config: buildRpxLbConfig(sites, appBoxes, { proxy: rpxProxy, slug }),
|
|
@@ -2763,6 +2802,8 @@ class HetznerDriver {
|
|
|
2763
2802
|
await this.waitForCloudInit(ip);
|
|
2764
2803
|
}
|
|
2765
2804
|
}
|
|
2805
|
+
} else {
|
|
2806
|
+
this.refreshLbRoutes(lbServer, sites, appBoxes, rpxProxy, slug);
|
|
2766
2807
|
}
|
|
2767
2808
|
lbId = lbServer.id;
|
|
2768
2809
|
lbIp = lbServer.public_net.ipv4?.ip;
|
|
@@ -2862,8 +2903,9 @@ class HetznerDriver {
|
|
|
2862
2903
|
const stackName = resolveProjectStackName(options.config, options.environment);
|
|
2863
2904
|
const state = await readDriverState(stackName);
|
|
2864
2905
|
if (state?.serverId) {
|
|
2865
|
-
const server = await this.
|
|
2866
|
-
|
|
2906
|
+
const server = await this.tryGetServer(state.serverId);
|
|
2907
|
+
if (server)
|
|
2908
|
+
return this.outputsFromState(state, server);
|
|
2867
2909
|
}
|
|
2868
2910
|
if (state?.lbServerId) {
|
|
2869
2911
|
const lb = await this.tryGetServer(state.lbServerId);
|
|
@@ -3008,6 +3050,21 @@ class HetznerDriver {
|
|
|
3008
3050
|
return null;
|
|
3009
3051
|
}
|
|
3010
3052
|
}
|
|
3053
|
+
refreshLbRoutes(lb, sites, appBoxes, proxy, slug) {
|
|
3054
|
+
const ip = lb.public_net.ipv4?.ip;
|
|
3055
|
+
if (!ip) {
|
|
3056
|
+
console.warn(`[ts-cloud] LB server '${lb.name}' has no public IP — skipping the rpx route refresh; its routes may be stale.`);
|
|
3057
|
+
return;
|
|
3058
|
+
}
|
|
3059
|
+
const routable = appBoxes.filter((box) => box.privateIp || box.publicIp);
|
|
3060
|
+
if (routable.length === 0) {
|
|
3061
|
+
console.warn(`[ts-cloud] No routable app boxes found for LB '${lb.name}' — leaving its current rpx routes untouched.`);
|
|
3062
|
+
return;
|
|
3063
|
+
}
|
|
3064
|
+
const config = buildRpxLbConfig(sites, routable, { proxy, slug });
|
|
3065
|
+
this.sshExec(ip, buildRpxFragmentRefreshScript({ config, slug }).join(`
|
|
3066
|
+
`));
|
|
3067
|
+
}
|
|
3011
3068
|
async findExistingServer(slug, environment, serverName) {
|
|
3012
3069
|
const servers = await this.client.listServers();
|
|
3013
3070
|
const exact = servers.find((server) => matchesTsCloudLabels(server.labels, slug, environment, "app") || server.name === serverName);
|
|
@@ -3066,12 +3123,12 @@ class HetznerDriver {
|
|
|
3066
3123
|
const start = Date.now();
|
|
3067
3124
|
while (Date.now() - start < cloudInitTimeoutMs) {
|
|
3068
3125
|
try {
|
|
3069
|
-
const out = this.sshExec(host,
|
|
3070
|
-
if (/status:\s*done/.test(out))
|
|
3071
|
-
return;
|
|
3126
|
+
const out = this.sshExec(host, `if command -v cloud-init >/dev/null 2>&1; then cloud-init status --long 2>/dev/null || true; else echo 'status: done'; fi`);
|
|
3072
3127
|
if (/status:\s*error/.test(out))
|
|
3073
3128
|
throw new Error(`cloud-init reported an error on ${host}:
|
|
3074
3129
|
${out}`);
|
|
3130
|
+
if (/status:\s*(?:done|degraded)/.test(out))
|
|
3131
|
+
return;
|
|
3075
3132
|
} catch (err) {
|
|
3076
3133
|
if (err instanceof Error && /cloud-init reported an error/.test(err.message))
|
|
3077
3134
|
throw err;
|
|
@@ -3164,7 +3221,9 @@ class LocalBoxDriver {
|
|
|
3164
3221
|
async uploadRelease(options) {
|
|
3165
3222
|
return { artifactRef: options.localPath };
|
|
3166
3223
|
}
|
|
3167
|
-
async findComputeTargets(
|
|
3224
|
+
async findComputeTargets(options) {
|
|
3225
|
+
if ((options.role || "app") !== "app")
|
|
3226
|
+
return [];
|
|
3168
3227
|
return [{ id: "localhost", name: "localhost", publicIp: "127.0.0.1", status: "running" }];
|
|
3169
3228
|
}
|
|
3170
3229
|
async runRemoteDeploy(options) {
|
|
@@ -3500,7 +3559,7 @@ class AwsBoxProvisioner {
|
|
|
3500
3559
|
MinCount: 1,
|
|
3501
3560
|
MaxCount: 1,
|
|
3502
3561
|
SecurityGroupIds: [groupId],
|
|
3503
|
-
UserData:
|
|
3562
|
+
UserData: Buffer.from(buildBoxUserData(spec), "utf8").toString("base64"),
|
|
3504
3563
|
TagSpecifications: [{ ResourceType: "instance", Tags: tags }]
|
|
3505
3564
|
});
|
|
3506
3565
|
const instanceId = result.Instances?.[0]?.InstanceId;
|
|
@@ -3717,20 +3776,24 @@ function buildSiteDeployScript(options) {
|
|
|
3717
3776
|
const paths = releasePaths(base, releaseId);
|
|
3718
3777
|
const unitBase = `${slug}-${siteName}`;
|
|
3719
3778
|
const serviceName = `${unitBase}.service`;
|
|
3779
|
+
const tarball = releaseTarballTmpPath(slug, siteName, releaseId);
|
|
3720
3780
|
const sharedPaths = [...new Set([".env", ...options.sharedPaths ?? []])];
|
|
3721
3781
|
const envFile = formatEnvFile(envEntries);
|
|
3722
3782
|
const preStart = preStartCommands.length > 0 ? [`cd ${paths.release}`, ...preStartCommands] : [];
|
|
3723
3783
|
const stageRelease = [
|
|
3724
3784
|
"set -euo pipefail",
|
|
3785
|
+
`trap 'if [ $? -ne 0 ] && [ "$(readlink -f ${paths.current} 2>/dev/null || true)" != "${paths.release}" ]; then rm -rf ${paths.release}; fi' EXIT`,
|
|
3725
3786
|
...artifactFetch,
|
|
3726
3787
|
...buildEnsureReleaseLayout(paths, sharedPaths),
|
|
3727
3788
|
`rm -rf ${paths.release}`,
|
|
3728
3789
|
`mkdir -p ${paths.release}`,
|
|
3729
|
-
`tar xzf
|
|
3790
|
+
`tar xzf ${tarball} -C ${paths.release}`,
|
|
3791
|
+
`rm -f ${tarball}`,
|
|
3730
3792
|
`cat > ${paths.shared}/.env <<'TS_CLOUD_ENV_EOF'`,
|
|
3731
3793
|
envFile,
|
|
3732
3794
|
"TS_CLOUD_ENV_EOF",
|
|
3733
3795
|
`chmod 600 ${paths.shared}/.env`,
|
|
3796
|
+
`for TS_CLOUD_ENV in ${paths.shared}/.env ${paths.release}/.env ${paths.release}/.env.*; do [ -f "$TS_CLOUD_ENV" ] && sed -i -E '/^[[:space:]]*(PORT|PORT_BACKEND|PORT_ADMIN|PORT_FRONTEND)[[:space:]]*=/d' "$TS_CLOUD_ENV" 2>/dev/null || true; done`,
|
|
3734
3797
|
...buildLinkSharedPaths(paths, sharedPaths),
|
|
3735
3798
|
...preStart
|
|
3736
3799
|
];
|
|
@@ -3767,7 +3830,7 @@ function buildSiteDeployScript(options) {
|
|
|
3767
3830
|
...buildActivateRelease(paths),
|
|
3768
3831
|
`systemctl enable ${instance} 2>/dev/null || true`,
|
|
3769
3832
|
`for TS_CLOUD_U in \${TS_CLOUD_OLD_UNITS}; do systemctl stop "$TS_CLOUD_U" 2>/dev/null || true; systemctl disable "$TS_CLOUD_U" 2>/dev/null || true; done`,
|
|
3770
|
-
`systemctl list-unit-files --plain --no-legend "${unitBase}@*.service" 2>/dev/null | awk '{print $1}' | grep -v "^${instance}$" | while read -r TS_CLOUD_U; do systemctl disable "$TS_CLOUD_U" 2>/dev/null || true; done`,
|
|
3833
|
+
`systemctl list-unit-files --plain --no-legend "${unitBase}@*.service" 2>/dev/null | awk '{print $1}' | grep -v -e "^${instance}$" -e "^${unitBase}@\\.service$" | while read -r TS_CLOUD_U; do systemctl disable "$TS_CLOUD_U" 2>/dev/null || true; done`,
|
|
3771
3834
|
`if [ -f /etc/systemd/system/${serviceName} ]; then systemctl disable ${serviceName} 2>/dev/null || true; rm -f /etc/systemd/system/${serviceName}; systemctl daemon-reload; fi`,
|
|
3772
3835
|
...buildPruneReleases(paths, keepReleases)
|
|
3773
3836
|
];
|
|
@@ -3803,27 +3866,34 @@ function buildStaticSiteDeployScript(options) {
|
|
|
3803
3866
|
const { siteName, artifactFetch, releaseId, keepReleases = DEFAULT_KEEP_RELEASES, preStartCommands = [] } = options;
|
|
3804
3867
|
const base = options.appDir ?? `/var/www/${siteName}`;
|
|
3805
3868
|
const paths = releasePaths(base, releaseId);
|
|
3869
|
+
const tarball = releaseTarballTmpPath(options.slug, siteName, releaseId);
|
|
3806
3870
|
const preStart = preStartCommands.length > 0 ? [`cd ${paths.release}`, ...preStartCommands] : [];
|
|
3807
3871
|
return [
|
|
3808
3872
|
"set -euo pipefail",
|
|
3873
|
+
`trap 'if [ $? -ne 0 ] && [ "$(readlink -f ${paths.current} 2>/dev/null || true)" != "${paths.release}" ]; then rm -rf ${paths.release}; fi' EXIT`,
|
|
3809
3874
|
...artifactFetch,
|
|
3810
3875
|
...buildEnsureReleaseLayout(paths, []),
|
|
3811
3876
|
`rm -rf ${paths.release}`,
|
|
3812
3877
|
`mkdir -p ${paths.release}`,
|
|
3813
|
-
`tar xzf
|
|
3878
|
+
`tar xzf ${tarball} -C ${paths.release}`,
|
|
3879
|
+
`rm -f ${tarball}`,
|
|
3814
3880
|
...preStart,
|
|
3815
3881
|
...buildActivateRelease(paths),
|
|
3816
3882
|
...buildPruneReleases(paths, keepReleases)
|
|
3817
3883
|
];
|
|
3818
3884
|
}
|
|
3819
|
-
function
|
|
3885
|
+
function releaseTarballTmpPath(slug, siteName, releaseId) {
|
|
3886
|
+
const parts = [slug, siteName, releaseId].filter(Boolean).join("-");
|
|
3887
|
+
return `/tmp/${parts}-release.tar.gz`;
|
|
3888
|
+
}
|
|
3889
|
+
function buildAwsArtifactFetch(bucket, key, region, destPath) {
|
|
3820
3890
|
return [
|
|
3821
|
-
`aws s3 cp "s3://${bucket}/${key}"
|
|
3891
|
+
`aws s3 cp "s3://${bucket}/${key}" ${destPath} --region ${region}`
|
|
3822
3892
|
];
|
|
3823
3893
|
}
|
|
3824
|
-
function buildLocalArtifactFetch(localPath,
|
|
3894
|
+
function buildLocalArtifactFetch(localPath, destPath) {
|
|
3825
3895
|
return [
|
|
3826
|
-
`cp "${localPath}"
|
|
3896
|
+
`cp "${localPath}" ${destPath}`
|
|
3827
3897
|
];
|
|
3828
3898
|
}
|
|
3829
3899
|
// src/drivers/shared/compute-deploy.ts
|
|
@@ -4087,7 +4157,7 @@ function buildCertbotInstallScript(dns) {
|
|
|
4087
4157
|
"mkdir -p /etc/letsencrypt/renewal-hooks/deploy",
|
|
4088
4158
|
"cat > /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh <<'TS_CLOUD_HOOK_EOF'",
|
|
4089
4159
|
"#!/bin/sh",
|
|
4090
|
-
"systemctl reload nginx",
|
|
4160
|
+
"systemctl reload ts-cloud-nginx 2>/dev/null || systemctl reload nginx 2>/dev/null || true",
|
|
4091
4161
|
"TS_CLOUD_HOOK_EOF",
|
|
4092
4162
|
"chmod +x /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh",
|
|
4093
4163
|
"systemctl enable certbot.timer 2>/dev/null || true",
|
|
@@ -4120,7 +4190,7 @@ function buildCertbotIssueScript(options) {
|
|
|
4120
4190
|
args.push(`--${plugin}-propagation-seconds ${dns.propagationSeconds}`);
|
|
4121
4191
|
args.push("certonly");
|
|
4122
4192
|
} else {
|
|
4123
|
-
args.push("--nginx", options.redirect === false ? "--no-redirect" : "--redirect");
|
|
4193
|
+
args.push("--nginx", `--nginx-ctl ${NGINX_WRAPPER}`, options.redirect === false ? "--no-redirect" : "--redirect");
|
|
4124
4194
|
}
|
|
4125
4195
|
if (options.email)
|
|
4126
4196
|
args.push(`-m ${options.email}`);
|
|
@@ -4164,16 +4234,16 @@ function buildGitCheckoutScript(options) {
|
|
|
4164
4234
|
];
|
|
4165
4235
|
if (repository.strategy === "tag") {
|
|
4166
4236
|
if (repository.tag) {
|
|
4167
|
-
lines.push(`git clone -q --depth 1 --branch ${repository.tag} ${url} ${releaseDir}`);
|
|
4237
|
+
lines.push(`git clone -q --depth 1 --branch ${shellQuote(repository.tag)} ${shellQuote(url)} ${releaseDir}`);
|
|
4168
4238
|
lines.push(`printf '%s' ${shellQuote(repository.tag)} > ${releaseDir}/.ts-cloud-tag`);
|
|
4169
4239
|
} else {
|
|
4170
4240
|
const pattern = repository.tagPattern || DEFAULT_TAG_PATTERN;
|
|
4171
|
-
lines.push(`TS_CLOUD_TAG="$(git ls-remote --tags --refs --sort=-v:refname ${url} ${shellQuote(`refs/tags/${pattern}`)} | head -n1 | sed 's#.*refs/tags/##')"`, `test -n "$TS_CLOUD_TAG" || { echo "no tags matching ${pattern} found in ${url}
|
|
4241
|
+
lines.push(`TS_CLOUD_TAG="$(git ls-remote --tags --refs --sort=-v:refname ${shellQuote(url)} ${shellQuote(`refs/tags/${pattern}`)} | head -n1 | sed 's#.*refs/tags/##')"`, `test -n "$TS_CLOUD_TAG" || { echo "no tags matching" ${shellQuote(pattern)} "found in" ${shellQuote(url)} >&2; exit 1; }`, `git clone -q --depth 1 --branch "$TS_CLOUD_TAG" ${shellQuote(url)} ${releaseDir}`, `printf '%s' "$TS_CLOUD_TAG" > ${releaseDir}/.ts-cloud-tag`);
|
|
4172
4242
|
}
|
|
4173
4243
|
} else if (commit) {
|
|
4174
|
-
lines.push(`git -C ${releaseDir} init -q`, `git -C ${releaseDir} remote add origin ${url}`, `git -C ${releaseDir} fetch -q --depth 1 origin ${commit}`, `git -C ${releaseDir} checkout -q FETCH_HEAD`);
|
|
4244
|
+
lines.push(`git -C ${releaseDir} init -q`, `git -C ${releaseDir} remote add origin ${shellQuote(url)}`, `git -C ${releaseDir} fetch -q --depth 1 origin ${shellQuote(commit)}`, `git -C ${releaseDir} checkout -q FETCH_HEAD`);
|
|
4175
4245
|
} else {
|
|
4176
|
-
lines.push(`git clone -q --depth 1 --branch ${branch} ${url} ${releaseDir}`);
|
|
4246
|
+
lines.push(`git clone -q --depth 1 --branch ${shellQuote(branch)} ${shellQuote(url)} ${releaseDir}`);
|
|
4177
4247
|
}
|
|
4178
4248
|
lines.push(`git -C ${releaseDir} rev-parse HEAD > ${releaseDir}/.ts-cloud-sha`);
|
|
4179
4249
|
return lines;
|
|
@@ -4589,12 +4659,13 @@ async function deploySiteRelease(driver, options, logger = noopLogger2) {
|
|
|
4589
4659
|
const hint = driver.name === "aws" ? `Stack '${stackName}' has no EC2 instances tagged Project=${slug} Environment=${environment} Role=app, and storage/cloud/state/${stackName}.json pins no live instance. For a shared box, record its instanceId there.` : `No Hetzner servers labeled ts-cloud/project=${slug} ts-cloud/environment=${environment} ts-cloud/role=app, and storage/cloud/state/${stackName}.json pins no live server. For a shared box, record its serverId there.`;
|
|
4590
4660
|
return { success: false, error: hint };
|
|
4591
4661
|
}
|
|
4662
|
+
const dbEnv = outputs.servicesPrivateIp ? buildFleetServicesEnv(outputs.servicesPrivateIp, resolveAppDatabase(config)) : buildManagedDbEnv(resolveAppDatabase(config));
|
|
4663
|
+
const envWithServices = Object.keys(dbEnv).length > 0 ? { ...dbEnv, ...site.env || {} } : site.env || {};
|
|
4592
4664
|
if (isPhpSite(site)) {
|
|
4593
4665
|
const compute2 = config.infrastructure?.compute;
|
|
4594
4666
|
const phpVersion = site.phpVersion ?? compute2?.php?.default ?? compute2?.php?.versions?.[0];
|
|
4595
4667
|
const appBase2 = siteInstallBase(slug, siteName);
|
|
4596
|
-
const
|
|
4597
|
-
const siteWithEnv = Object.keys(dbEnv).length > 0 ? { ...site, env: { ...dbEnv, ...site.env || {} } } : site;
|
|
4668
|
+
const siteWithEnv = Object.keys(dbEnv).length > 0 ? { ...site, env: envWithServices } : site;
|
|
4598
4669
|
const deployScript = buildLaravelDeployScript({
|
|
4599
4670
|
siteName,
|
|
4600
4671
|
site: siteWithEnv,
|
|
@@ -4661,11 +4732,13 @@ async function deploySiteRelease(driver, options, logger = noopLogger2) {
|
|
|
4661
4732
|
remoteKey,
|
|
4662
4733
|
targets
|
|
4663
4734
|
});
|
|
4664
|
-
const
|
|
4735
|
+
const tarballPath = releaseTarballTmpPath(slug, siteName, sha);
|
|
4736
|
+
const artifactFetch = driver.name === "aws" ? buildAwsArtifactFetch(outputs.deployBucketName, remoteKey, config.project.region || "us-east-1", tarballPath) : buildLocalArtifactFetch(uploadResult.artifactRef, tarballPath);
|
|
4665
4737
|
const kind = resolveSiteKind(site);
|
|
4666
4738
|
const appBase = siteInstallBase(slug, siteName);
|
|
4667
4739
|
const baseScript = kind === "server-static" ? buildStaticSiteDeployScript({
|
|
4668
4740
|
siteName,
|
|
4741
|
+
slug,
|
|
4669
4742
|
appDir: appBase,
|
|
4670
4743
|
artifactFetch,
|
|
4671
4744
|
releaseId: sha,
|
|
@@ -4677,7 +4750,7 @@ async function deploySiteRelease(driver, options, logger = noopLogger2) {
|
|
|
4677
4750
|
artifactFetch,
|
|
4678
4751
|
releaseId: sha,
|
|
4679
4752
|
execStart: resolveExecStart(site.start, runtime),
|
|
4680
|
-
envEntries:
|
|
4753
|
+
envEntries: envWithServices,
|
|
4681
4754
|
port: site.port,
|
|
4682
4755
|
preStartCommands: site.preStart,
|
|
4683
4756
|
sharedPaths: site.sharedPaths,
|
|
@@ -4707,7 +4780,7 @@ async function deploySiteRelease(driver, options, logger = noopLogger2) {
|
|
|
4707
4780
|
...buildDeployHistoryHeader(appBase, {
|
|
4708
4781
|
releaseId: sha,
|
|
4709
4782
|
commit: sha,
|
|
4710
|
-
branch: site.branch ?? "main"
|
|
4783
|
+
branch: site.repository?.branch ?? "main"
|
|
4711
4784
|
}),
|
|
4712
4785
|
...buildSiteOwnerGuard(appBase, slug),
|
|
4713
4786
|
...baseScript,
|
|
@@ -4740,6 +4813,44 @@ async function deploySiteRelease(driver, options, logger = noopLogger2) {
|
|
|
4740
4813
|
perInstance: result.perInstance
|
|
4741
4814
|
};
|
|
4742
4815
|
}
|
|
4816
|
+
async function ensureAttachModeDatabase(driver, options, logger) {
|
|
4817
|
+
const { config, environment } = options;
|
|
4818
|
+
if (!config.cloud?.attachTo)
|
|
4819
|
+
return true;
|
|
4820
|
+
const compute = config.infrastructure?.compute;
|
|
4821
|
+
const database = resolveAppDatabase(config);
|
|
4822
|
+
const commands = buildDatabaseSetupScript(database, compute?.managedServices ?? {});
|
|
4823
|
+
if (commands.length === 0)
|
|
4824
|
+
return true;
|
|
4825
|
+
const slug = config.project.slug;
|
|
4826
|
+
const stackName = resolveProjectStackName(config, environment);
|
|
4827
|
+
const targets = await driver.findComputeTargets({
|
|
4828
|
+
slug,
|
|
4829
|
+
environment,
|
|
4830
|
+
role: "app",
|
|
4831
|
+
stackName
|
|
4832
|
+
});
|
|
4833
|
+
if (targets.length === 0) {
|
|
4834
|
+
return true;
|
|
4835
|
+
}
|
|
4836
|
+
logger.step(`Ensuring database '${database?.name}' + role exist on the shared box...`);
|
|
4837
|
+
const result = await driver.runRemoteDeploy({
|
|
4838
|
+
targets,
|
|
4839
|
+
commands,
|
|
4840
|
+
comment: `ts-cloud ensure database ${slug}/${database?.name}`,
|
|
4841
|
+
tags: {
|
|
4842
|
+
Project: slug,
|
|
4843
|
+
Environment: environment,
|
|
4844
|
+
Role: "app"
|
|
4845
|
+
}
|
|
4846
|
+
});
|
|
4847
|
+
if (!result.success) {
|
|
4848
|
+
logger.error(`Ensuring database '${database?.name}' failed: ${result.error || "unknown error"}`);
|
|
4849
|
+
return false;
|
|
4850
|
+
}
|
|
4851
|
+
logger.success(`Database '${database?.name}' is ready on ${result.instanceCount} target(s)`);
|
|
4852
|
+
return true;
|
|
4853
|
+
}
|
|
4743
4854
|
async function deployAllComputeSites(options) {
|
|
4744
4855
|
const { config, environment, driver, sha, runtime, tarballForSite, logger = noopLogger2 } = options;
|
|
4745
4856
|
const slug = config.project.slug;
|
|
@@ -4790,6 +4901,8 @@ async function deployAllComputeSites(options) {
|
|
|
4790
4901
|
});
|
|
4791
4902
|
if (deployable.length === 0)
|
|
4792
4903
|
return true;
|
|
4904
|
+
if (!await ensureAttachModeDatabase(driver, options, logger))
|
|
4905
|
+
return false;
|
|
4793
4906
|
const tarballFor = (siteName) => dashboardTarballBySite.get(siteName) ?? tarballForSite(siteName);
|
|
4794
4907
|
for (const [siteName, site] of deployable) {
|
|
4795
4908
|
logger.step(`Deploying site: ${siteName}`);
|
|
@@ -4832,29 +4945,68 @@ async function reloadRpxGateway(options) {
|
|
|
4832
4945
|
if (proxy?.engine !== "rpx")
|
|
4833
4946
|
return true;
|
|
4834
4947
|
const sites = routeSource.sites || {};
|
|
4835
|
-
const
|
|
4948
|
+
const slug = config.project.slug;
|
|
4949
|
+
const stackName = resolveProjectStackName(config, environment);
|
|
4950
|
+
const lbTargets = await driver.findComputeTargets({
|
|
4951
|
+
slug,
|
|
4952
|
+
environment,
|
|
4953
|
+
role: "lb",
|
|
4954
|
+
stackName
|
|
4955
|
+
});
|
|
4956
|
+
if (lbTargets.length > 0) {
|
|
4957
|
+
const appTargets = await driver.findComputeTargets({
|
|
4958
|
+
slug,
|
|
4959
|
+
environment,
|
|
4960
|
+
role: "app",
|
|
4961
|
+
stackName
|
|
4962
|
+
});
|
|
4963
|
+
const appBoxes = appTargets.map((t) => ({ privateIp: t.privateIp, publicIp: t.publicIp }));
|
|
4964
|
+
const lbConfig = buildRpxLbConfig(sites, appBoxes, { proxy, slug });
|
|
4965
|
+
if (lbConfig.proxies.length === 0) {
|
|
4966
|
+
logger.warn("rpx gateway: no server sites with a domain to route — skipping gateway reload.");
|
|
4967
|
+
return true;
|
|
4968
|
+
}
|
|
4969
|
+
logger.step(`Reloading the load balancer's rpx gateway with ${lbConfig.proxies.length} route(s)...`);
|
|
4970
|
+
const result2 = await driver.runRemoteDeploy({
|
|
4971
|
+
targets: lbTargets,
|
|
4972
|
+
commands: buildRpxFragmentRefreshScript({ config: lbConfig, slug }),
|
|
4973
|
+
comment: `ts-cloud rpx gateway reload ${slug}`,
|
|
4974
|
+
tags: {
|
|
4975
|
+
Project: slug,
|
|
4976
|
+
Environment: environment,
|
|
4977
|
+
Role: "lb"
|
|
4978
|
+
}
|
|
4979
|
+
});
|
|
4980
|
+
if (!result2.success) {
|
|
4981
|
+
logger.error(`rpx gateway reload failed: ${result2.error || "unknown error"}`);
|
|
4982
|
+
return false;
|
|
4983
|
+
}
|
|
4984
|
+
logger.success(`rpx gateway reloaded on ${result2.instanceCount} load balancer(s)`);
|
|
4985
|
+
return true;
|
|
4986
|
+
}
|
|
4987
|
+
const rpxConfig = buildRpxConfig(sites, { proxy, slug });
|
|
4836
4988
|
if (rpxConfig.proxies.length === 0) {
|
|
4837
4989
|
logger.warn("rpx gateway: no server sites with a domain to route — skipping gateway reload.");
|
|
4838
4990
|
return true;
|
|
4839
4991
|
}
|
|
4840
4992
|
const targets = await driver.findComputeTargets({
|
|
4841
|
-
slug
|
|
4993
|
+
slug,
|
|
4842
4994
|
environment,
|
|
4843
4995
|
role: "app",
|
|
4844
|
-
stackName
|
|
4996
|
+
stackName
|
|
4845
4997
|
});
|
|
4846
4998
|
if (targets.length === 0) {
|
|
4847
4999
|
logger.warn("rpx gateway: no compute targets found — skipping gateway reload.");
|
|
4848
5000
|
return true;
|
|
4849
5001
|
}
|
|
4850
5002
|
logger.step(`Reloading rpx gateway with ${rpxConfig.proxies.length} route(s)...`);
|
|
4851
|
-
const script = buildRpxProvisionScript({ proxy, config: rpxConfig, slug
|
|
5003
|
+
const script = buildRpxProvisionScript({ proxy, config: rpxConfig, slug });
|
|
4852
5004
|
const result = await driver.runRemoteDeploy({
|
|
4853
5005
|
targets,
|
|
4854
5006
|
commands: script,
|
|
4855
|
-
comment: `ts-cloud rpx gateway reload ${
|
|
5007
|
+
comment: `ts-cloud rpx gateway reload ${slug}`,
|
|
4856
5008
|
tags: {
|
|
4857
|
-
Project:
|
|
5009
|
+
Project: slug,
|
|
4858
5010
|
Environment: environment,
|
|
4859
5011
|
Role: "app"
|
|
4860
5012
|
}
|
|
@@ -4946,4 +5098,4 @@ function buildCloudFrontOriginConfig(options) {
|
|
|
4946
5098
|
CustomErrorResponses: { Quantity: 0 }
|
|
4947
5099
|
};
|
|
4948
5100
|
}
|
|
4949
|
-
export { siteInstallBase, isPhpSite, resolveSiteDeployTarget, resolveSiteKind, validateDeploymentConfig, PANTRY_PROJECT_DIR, DEFAULT_RPX_CERTS_DIR, normalizeRoutePath, deriveRouteId, buildRpxConfig, buildRpxLbConfig, renderRpxLauncher, RPX_DIR, RPX_LAUNCHER_PATH, RPX_SERVICE_NAME, buildRpxProvisionScript, BACKUP_RUNNER_PATH, buildBackupRestoreScript, buildUbuntuBootstrapScript, AwsDriver, resolveHetznerLocation, HetznerClient, resolveHetznerApiToken2 as resolveHetznerApiToken, normalizeSshPublicKey, wrapCloudInitUserData, HetznerDriver, LocalBoxDriver, isBoxMode, createCloudDriver, CloudDriverFactory, cloudDrivers, ensureSshKey, ensureFirewall, ensureServer, serverPublicIpv4, buildSshArgs, sshExec, sshExecOrThrow, scpUpload, waitForSsh, waitForCloudInit, UBUNTU_2404_AMI_PARAM, buildBoxUserData, HetznerBoxProvisioner, AwsBoxProvisioner, createBoxProvisioner, releasePaths, buildRollbackScript, resolveExecStart, buildSiteDeployScript, buildStaticSiteDeployScript, buildAwsArtifactFetch, buildLocalArtifactFetch, MANAGEMENT_DASHBOARD_SITE, DASHBOARD_CREDENTIALS_FILE, resolveDashboardAuth, resolveUiSource, ensureManagementDashboard, buildManagementDashboardArtifact, resolveSiteFramework, deploySiteRelease, deployAllComputeSites, reloadRpxGateway, MANAGED_CACHE_POLICY_OPTIMIZED, MANAGED_CACHE_POLICY_DISABLED, MANAGED_ORIGIN_REQUEST_POLICY_ALL_VIEWER, buildCloudFrontOriginConfig };
|
|
5101
|
+
export { siteInstallBase, isPhpSite, resolveSiteDeployTarget, resolveSiteKind, validateDeploymentConfig, PANTRY_PROJECT_DIR, DEFAULT_RPX_CERTS_DIR, normalizeRoutePath, deriveRouteId, buildRpxConfig, buildRpxLbConfig, renderRpxLauncher, RPX_DIR, RPX_LAUNCHER_PATH, RPX_SERVICE_NAME, buildRpxProvisionScript, buildRpxFragmentRefreshScript, BACKUP_RUNNER_PATH, buildBackupRestoreScript, buildUbuntuBootstrapScript, AwsDriver, resolveHetznerLocation, HetznerClient, resolveHetznerApiToken2 as resolveHetznerApiToken, normalizeSshPublicKey, wrapCloudInitUserData, HetznerDriver, LocalBoxDriver, isBoxMode, createCloudDriver, CloudDriverFactory, cloudDrivers, ensureSshKey, ensureFirewall, ensureServer, serverPublicIpv4, buildSshArgs, sshExec, sshExecOrThrow, scpUpload, waitForSsh, waitForCloudInit, UBUNTU_2404_AMI_PARAM, buildBoxUserData, HetznerBoxProvisioner, AwsBoxProvisioner, createBoxProvisioner, releasePaths, buildRollbackScript, resolveExecStart, buildSiteDeployScript, buildStaticSiteDeployScript, releaseTarballTmpPath, buildAwsArtifactFetch, buildLocalArtifactFetch, MANAGEMENT_DASHBOARD_SITE, DASHBOARD_CREDENTIALS_FILE, resolveDashboardAuth, resolveUiSource, ensureManagementDashboard, buildManagementDashboardArtifact, resolveSiteFramework, deploySiteRelease, deployAllComputeSites, reloadRpxGateway, MANAGED_CACHE_POLICY_OPTIMIZED, MANAGED_CACHE_POLICY_DISABLED, MANAGED_ORIGIN_REQUEST_POLICY_ALL_VIEWER, buildCloudFrontOriginConfig };
|