@stacksjs/ts-cloud 0.7.30 → 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-xdd6qvre.js → chunk-0c69nbaa.js} +252 -101
- package/dist/{chunk-rr3j38qr.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,16 +3776,19 @@ 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",
|
|
@@ -3768,7 +3830,7 @@ function buildSiteDeployScript(options) {
|
|
|
3768
3830
|
...buildActivateRelease(paths),
|
|
3769
3831
|
`systemctl enable ${instance} 2>/dev/null || true`,
|
|
3770
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`,
|
|
3771
|
-
`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`,
|
|
3772
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`,
|
|
3773
3835
|
...buildPruneReleases(paths, keepReleases)
|
|
3774
3836
|
];
|
|
@@ -3804,27 +3866,34 @@ function buildStaticSiteDeployScript(options) {
|
|
|
3804
3866
|
const { siteName, artifactFetch, releaseId, keepReleases = DEFAULT_KEEP_RELEASES, preStartCommands = [] } = options;
|
|
3805
3867
|
const base = options.appDir ?? `/var/www/${siteName}`;
|
|
3806
3868
|
const paths = releasePaths(base, releaseId);
|
|
3869
|
+
const tarball = releaseTarballTmpPath(options.slug, siteName, releaseId);
|
|
3807
3870
|
const preStart = preStartCommands.length > 0 ? [`cd ${paths.release}`, ...preStartCommands] : [];
|
|
3808
3871
|
return [
|
|
3809
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`,
|
|
3810
3874
|
...artifactFetch,
|
|
3811
3875
|
...buildEnsureReleaseLayout(paths, []),
|
|
3812
3876
|
`rm -rf ${paths.release}`,
|
|
3813
3877
|
`mkdir -p ${paths.release}`,
|
|
3814
|
-
`tar xzf
|
|
3878
|
+
`tar xzf ${tarball} -C ${paths.release}`,
|
|
3879
|
+
`rm -f ${tarball}`,
|
|
3815
3880
|
...preStart,
|
|
3816
3881
|
...buildActivateRelease(paths),
|
|
3817
3882
|
...buildPruneReleases(paths, keepReleases)
|
|
3818
3883
|
];
|
|
3819
3884
|
}
|
|
3820
|
-
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) {
|
|
3821
3890
|
return [
|
|
3822
|
-
`aws s3 cp "s3://${bucket}/${key}"
|
|
3891
|
+
`aws s3 cp "s3://${bucket}/${key}" ${destPath} --region ${region}`
|
|
3823
3892
|
];
|
|
3824
3893
|
}
|
|
3825
|
-
function buildLocalArtifactFetch(localPath,
|
|
3894
|
+
function buildLocalArtifactFetch(localPath, destPath) {
|
|
3826
3895
|
return [
|
|
3827
|
-
`cp "${localPath}"
|
|
3896
|
+
`cp "${localPath}" ${destPath}`
|
|
3828
3897
|
];
|
|
3829
3898
|
}
|
|
3830
3899
|
// src/drivers/shared/compute-deploy.ts
|
|
@@ -4088,7 +4157,7 @@ function buildCertbotInstallScript(dns) {
|
|
|
4088
4157
|
"mkdir -p /etc/letsencrypt/renewal-hooks/deploy",
|
|
4089
4158
|
"cat > /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh <<'TS_CLOUD_HOOK_EOF'",
|
|
4090
4159
|
"#!/bin/sh",
|
|
4091
|
-
"systemctl reload nginx",
|
|
4160
|
+
"systemctl reload ts-cloud-nginx 2>/dev/null || systemctl reload nginx 2>/dev/null || true",
|
|
4092
4161
|
"TS_CLOUD_HOOK_EOF",
|
|
4093
4162
|
"chmod +x /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh",
|
|
4094
4163
|
"systemctl enable certbot.timer 2>/dev/null || true",
|
|
@@ -4121,7 +4190,7 @@ function buildCertbotIssueScript(options) {
|
|
|
4121
4190
|
args.push(`--${plugin}-propagation-seconds ${dns.propagationSeconds}`);
|
|
4122
4191
|
args.push("certonly");
|
|
4123
4192
|
} else {
|
|
4124
|
-
args.push("--nginx", options.redirect === false ? "--no-redirect" : "--redirect");
|
|
4193
|
+
args.push("--nginx", `--nginx-ctl ${NGINX_WRAPPER}`, options.redirect === false ? "--no-redirect" : "--redirect");
|
|
4125
4194
|
}
|
|
4126
4195
|
if (options.email)
|
|
4127
4196
|
args.push(`-m ${options.email}`);
|
|
@@ -4165,16 +4234,16 @@ function buildGitCheckoutScript(options) {
|
|
|
4165
4234
|
];
|
|
4166
4235
|
if (repository.strategy === "tag") {
|
|
4167
4236
|
if (repository.tag) {
|
|
4168
|
-
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}`);
|
|
4169
4238
|
lines.push(`printf '%s' ${shellQuote(repository.tag)} > ${releaseDir}/.ts-cloud-tag`);
|
|
4170
4239
|
} else {
|
|
4171
4240
|
const pattern = repository.tagPattern || DEFAULT_TAG_PATTERN;
|
|
4172
|
-
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`);
|
|
4173
4242
|
}
|
|
4174
4243
|
} else if (commit) {
|
|
4175
|
-
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`);
|
|
4176
4245
|
} else {
|
|
4177
|
-
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}`);
|
|
4178
4247
|
}
|
|
4179
4248
|
lines.push(`git -C ${releaseDir} rev-parse HEAD > ${releaseDir}/.ts-cloud-sha`);
|
|
4180
4249
|
return lines;
|
|
@@ -4590,12 +4659,13 @@ async function deploySiteRelease(driver, options, logger = noopLogger2) {
|
|
|
4590
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.`;
|
|
4591
4660
|
return { success: false, error: hint };
|
|
4592
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 || {};
|
|
4593
4664
|
if (isPhpSite(site)) {
|
|
4594
4665
|
const compute2 = config.infrastructure?.compute;
|
|
4595
4666
|
const phpVersion = site.phpVersion ?? compute2?.php?.default ?? compute2?.php?.versions?.[0];
|
|
4596
4667
|
const appBase2 = siteInstallBase(slug, siteName);
|
|
4597
|
-
const
|
|
4598
|
-
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;
|
|
4599
4669
|
const deployScript = buildLaravelDeployScript({
|
|
4600
4670
|
siteName,
|
|
4601
4671
|
site: siteWithEnv,
|
|
@@ -4662,11 +4732,13 @@ async function deploySiteRelease(driver, options, logger = noopLogger2) {
|
|
|
4662
4732
|
remoteKey,
|
|
4663
4733
|
targets
|
|
4664
4734
|
});
|
|
4665
|
-
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);
|
|
4666
4737
|
const kind = resolveSiteKind(site);
|
|
4667
4738
|
const appBase = siteInstallBase(slug, siteName);
|
|
4668
4739
|
const baseScript = kind === "server-static" ? buildStaticSiteDeployScript({
|
|
4669
4740
|
siteName,
|
|
4741
|
+
slug,
|
|
4670
4742
|
appDir: appBase,
|
|
4671
4743
|
artifactFetch,
|
|
4672
4744
|
releaseId: sha,
|
|
@@ -4678,7 +4750,7 @@ async function deploySiteRelease(driver, options, logger = noopLogger2) {
|
|
|
4678
4750
|
artifactFetch,
|
|
4679
4751
|
releaseId: sha,
|
|
4680
4752
|
execStart: resolveExecStart(site.start, runtime),
|
|
4681
|
-
envEntries:
|
|
4753
|
+
envEntries: envWithServices,
|
|
4682
4754
|
port: site.port,
|
|
4683
4755
|
preStartCommands: site.preStart,
|
|
4684
4756
|
sharedPaths: site.sharedPaths,
|
|
@@ -4708,7 +4780,7 @@ async function deploySiteRelease(driver, options, logger = noopLogger2) {
|
|
|
4708
4780
|
...buildDeployHistoryHeader(appBase, {
|
|
4709
4781
|
releaseId: sha,
|
|
4710
4782
|
commit: sha,
|
|
4711
|
-
branch: site.branch ?? "main"
|
|
4783
|
+
branch: site.repository?.branch ?? "main"
|
|
4712
4784
|
}),
|
|
4713
4785
|
...buildSiteOwnerGuard(appBase, slug),
|
|
4714
4786
|
...baseScript,
|
|
@@ -4741,6 +4813,44 @@ async function deploySiteRelease(driver, options, logger = noopLogger2) {
|
|
|
4741
4813
|
perInstance: result.perInstance
|
|
4742
4814
|
};
|
|
4743
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
|
+
}
|
|
4744
4854
|
async function deployAllComputeSites(options) {
|
|
4745
4855
|
const { config, environment, driver, sha, runtime, tarballForSite, logger = noopLogger2 } = options;
|
|
4746
4856
|
const slug = config.project.slug;
|
|
@@ -4791,6 +4901,8 @@ async function deployAllComputeSites(options) {
|
|
|
4791
4901
|
});
|
|
4792
4902
|
if (deployable.length === 0)
|
|
4793
4903
|
return true;
|
|
4904
|
+
if (!await ensureAttachModeDatabase(driver, options, logger))
|
|
4905
|
+
return false;
|
|
4794
4906
|
const tarballFor = (siteName) => dashboardTarballBySite.get(siteName) ?? tarballForSite(siteName);
|
|
4795
4907
|
for (const [siteName, site] of deployable) {
|
|
4796
4908
|
logger.step(`Deploying site: ${siteName}`);
|
|
@@ -4833,29 +4945,68 @@ async function reloadRpxGateway(options) {
|
|
|
4833
4945
|
if (proxy?.engine !== "rpx")
|
|
4834
4946
|
return true;
|
|
4835
4947
|
const sites = routeSource.sites || {};
|
|
4836
|
-
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 });
|
|
4837
4988
|
if (rpxConfig.proxies.length === 0) {
|
|
4838
4989
|
logger.warn("rpx gateway: no server sites with a domain to route — skipping gateway reload.");
|
|
4839
4990
|
return true;
|
|
4840
4991
|
}
|
|
4841
4992
|
const targets = await driver.findComputeTargets({
|
|
4842
|
-
slug
|
|
4993
|
+
slug,
|
|
4843
4994
|
environment,
|
|
4844
4995
|
role: "app",
|
|
4845
|
-
stackName
|
|
4996
|
+
stackName
|
|
4846
4997
|
});
|
|
4847
4998
|
if (targets.length === 0) {
|
|
4848
4999
|
logger.warn("rpx gateway: no compute targets found — skipping gateway reload.");
|
|
4849
5000
|
return true;
|
|
4850
5001
|
}
|
|
4851
5002
|
logger.step(`Reloading rpx gateway with ${rpxConfig.proxies.length} route(s)...`);
|
|
4852
|
-
const script = buildRpxProvisionScript({ proxy, config: rpxConfig, slug
|
|
5003
|
+
const script = buildRpxProvisionScript({ proxy, config: rpxConfig, slug });
|
|
4853
5004
|
const result = await driver.runRemoteDeploy({
|
|
4854
5005
|
targets,
|
|
4855
5006
|
commands: script,
|
|
4856
|
-
comment: `ts-cloud rpx gateway reload ${
|
|
5007
|
+
comment: `ts-cloud rpx gateway reload ${slug}`,
|
|
4857
5008
|
tags: {
|
|
4858
|
-
Project:
|
|
5009
|
+
Project: slug,
|
|
4859
5010
|
Environment: environment,
|
|
4860
5011
|
Role: "app"
|
|
4861
5012
|
}
|
|
@@ -4947,4 +5098,4 @@ function buildCloudFrontOriginConfig(options) {
|
|
|
4947
5098
|
CustomErrorResponses: { Quantity: 0 }
|
|
4948
5099
|
};
|
|
4949
5100
|
}
|
|
4950
|
-
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 };
|