@stacksjs/ts-cloud 0.5.17 → 0.5.19
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 +639 -601
- package/dist/deploy/dashboard-data.d.ts +1 -1
- package/dist/deploy/hooks.d.ts +25 -0
- package/dist/deploy/management-dashboard.d.ts +4 -4
- package/dist/deploy/quick-deploy.d.ts +25 -0
- package/dist/drivers/hetzner/driver.d.ts +1 -1
- package/dist/drivers/shared/backups.d.ts +12 -0
- package/dist/drivers/shared/compute-ops.d.ts +10 -1
- package/dist/drivers/shared/laravel-deploy.d.ts +8 -0
- package/dist/drivers/shared/monitoring.d.ts +15 -7
- package/dist/drivers/shared/nginx-vhost.d.ts +19 -0
- package/dist/drivers/shared/php-fpm-pool.d.ts +34 -0
- package/dist/index.js +225 -41
- package/dist/security/pre-deploy-scanner.d.ts +1 -1
- package/dist/ui/index.html +205 -68
- package/dist/ui/server/backups.html +205 -68
- package/dist/ui/server/deployments.html +205 -68
- package/dist/ui/server/metrics.html +205 -68
- package/dist/ui/server/services.html +205 -68
- package/dist/ui/server/sites.html +205 -68
- package/dist/ui/server/ssh-keys.html +205 -68
- package/dist/ui/server/workers.html +205 -68
- package/dist/ui/serverless/assets.html +205 -68
- package/dist/ui/serverless/cost.html +205 -68
- package/dist/ui/serverless/data.html +205 -68
- package/dist/ui/serverless/deployments.html +205 -68
- package/dist/ui/serverless/firewall.html +205 -68
- package/dist/ui/serverless/functions.html +205 -68
- package/dist/ui/serverless/metrics.html +205 -68
- package/dist/ui/serverless/queues.html +205 -68
- package/dist/ui/serverless/scheduler.html +205 -68
- package/dist/ui/serverless/secrets.html +205 -68
- package/dist/ui/serverless.html +205 -68
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -7642,11 +7642,11 @@ function createServerlessLaravelPreset(options) {
|
|
|
7642
7642
|
}
|
|
7643
7643
|
function createDashboardSite(options) {
|
|
7644
7644
|
return {
|
|
7645
|
-
root: options.root ?? "ui/dist",
|
|
7645
|
+
root: options.root ?? "packages/ui/dist",
|
|
7646
7646
|
deploy: "server",
|
|
7647
7647
|
type: "static",
|
|
7648
7648
|
domain: options.domain,
|
|
7649
|
-
build: options.build ?? "cd ui && bun install && bun run build",
|
|
7649
|
+
build: options.build ?? "cd packages/ui && bun install && bun run build",
|
|
7650
7650
|
ssl: { provider: "letsencrypt" },
|
|
7651
7651
|
auth: {
|
|
7652
7652
|
username: options.username ?? "admin",
|
|
@@ -8440,18 +8440,19 @@ class S3Client {
|
|
|
8440
8440
|
stream = blob.stream();
|
|
8441
8441
|
totalSize = blob.size;
|
|
8442
8442
|
}
|
|
8443
|
-
const uploadId = await this.initiateMultipartUpload(bucket, key, options);
|
|
8443
|
+
const { uploadId, baseUrl } = await this.initiateMultipartUpload(bucket, key, options);
|
|
8444
8444
|
try {
|
|
8445
|
-
const parts = await this.uploadParts(bucket, key, uploadId, stream, partSize, credentials, totalSize, options.concurrency || 4, options.onProgress);
|
|
8446
|
-
return await this.completeMultipartUpload(bucket, key, uploadId, parts);
|
|
8445
|
+
const parts = await this.uploadParts(bucket, key, baseUrl, uploadId, stream, partSize, credentials, totalSize, options.concurrency || 4, options.onProgress);
|
|
8446
|
+
return await this.completeMultipartUpload(bucket, key, baseUrl, uploadId, parts);
|
|
8447
8447
|
} catch (error) {
|
|
8448
|
-
await this.abortMultipartUpload(bucket, key, uploadId).catch(() => {});
|
|
8448
|
+
await this.abortMultipartUpload(bucket, key, uploadId, baseUrl).catch(() => {});
|
|
8449
8449
|
throw error;
|
|
8450
8450
|
}
|
|
8451
8451
|
}
|
|
8452
8452
|
async initiateMultipartUpload(bucket, key, options) {
|
|
8453
8453
|
const credentials = await this.getCredentials();
|
|
8454
|
-
const
|
|
8454
|
+
const baseUrl = this.buildUrl(bucket, key);
|
|
8455
|
+
const url = `${baseUrl}?uploads`;
|
|
8455
8456
|
const headers = {
|
|
8456
8457
|
"Content-Type": options.contentType || detectContentType(key)
|
|
8457
8458
|
};
|
|
@@ -8487,18 +8488,26 @@ class S3Client {
|
|
|
8487
8488
|
if (!uploadIdMatch) {
|
|
8488
8489
|
throw new S3Error("Failed to parse upload ID from response", 0, bucket, key);
|
|
8489
8490
|
}
|
|
8490
|
-
|
|
8491
|
+
let resolvedBaseUrl = baseUrl;
|
|
8492
|
+
if (response.url) {
|
|
8493
|
+
try {
|
|
8494
|
+
const resolved = new URL(response.url);
|
|
8495
|
+
resolved.search = "";
|
|
8496
|
+
resolvedBaseUrl = resolved.toString();
|
|
8497
|
+
} catch {}
|
|
8498
|
+
}
|
|
8499
|
+
return { uploadId: uploadIdMatch[1], baseUrl: resolvedBaseUrl };
|
|
8491
8500
|
}
|
|
8492
|
-
async uploadParts(bucket, key, uploadId, stream, partSize, credentials, totalSize, concurrency, onProgress) {
|
|
8501
|
+
async uploadParts(bucket, key, baseUrl, uploadId, stream, partSize, credentials, totalSize, concurrency, onProgress) {
|
|
8493
8502
|
const parts = [];
|
|
8494
8503
|
const reader = stream.getReader();
|
|
8495
8504
|
let partNumber = 1;
|
|
8496
8505
|
let buffer = new Uint8Array(0);
|
|
8497
8506
|
let loaded = 0;
|
|
8498
8507
|
const totalParts = totalSize ? Math.ceil(totalSize / partSize) : undefined;
|
|
8499
|
-
const
|
|
8508
|
+
const inFlight = new Map;
|
|
8500
8509
|
const uploadPart = async (data, num) => {
|
|
8501
|
-
const url = `${
|
|
8510
|
+
const url = `${baseUrl}?partNumber=${num}&uploadId=${encodeURIComponent(uploadId)}`;
|
|
8502
8511
|
const headers = {
|
|
8503
8512
|
"Content-Length": String(data.byteLength),
|
|
8504
8513
|
"x-amz-content-sha256": "UNSIGNED-PAYLOAD"
|
|
@@ -8514,15 +8523,24 @@ class S3Client {
|
|
|
8514
8523
|
const response = await fetch(signed.url, {
|
|
8515
8524
|
method: signed.method,
|
|
8516
8525
|
headers: signed.headers,
|
|
8517
|
-
body: data
|
|
8526
|
+
body: data,
|
|
8527
|
+
redirect: "error"
|
|
8518
8528
|
});
|
|
8519
8529
|
if (!response.ok) {
|
|
8520
8530
|
const error = await response.text();
|
|
8521
8531
|
throw new S3Error(`Failed to upload part ${num}: ${error}`, response.status, bucket, key);
|
|
8522
8532
|
}
|
|
8523
8533
|
const etag = (response.headers.get("ETag") || "").replace(/"/g, "");
|
|
8534
|
+
if (!etag) {
|
|
8535
|
+
throw new S3Error(`Missing ETag for part ${num}`, response.status, bucket, key);
|
|
8536
|
+
}
|
|
8524
8537
|
return { partNumber: num, etag };
|
|
8525
8538
|
};
|
|
8539
|
+
const drainOne = async () => {
|
|
8540
|
+
const completed = await Promise.race(inFlight.values());
|
|
8541
|
+
inFlight.delete(completed.partNumber);
|
|
8542
|
+
parts.push(completed);
|
|
8543
|
+
};
|
|
8526
8544
|
while (true) {
|
|
8527
8545
|
const { done, value } = await reader.read();
|
|
8528
8546
|
if (value) {
|
|
@@ -8535,13 +8553,10 @@ class S3Client {
|
|
|
8535
8553
|
const partData = buffer.slice(0, partSize);
|
|
8536
8554
|
buffer = buffer.slice(partSize);
|
|
8537
8555
|
const currentPartNumber = partNumber++;
|
|
8538
|
-
if (
|
|
8539
|
-
|
|
8540
|
-
parts.push(completed);
|
|
8541
|
-
uploadQueue.splice(uploadQueue.indexOf(Promise.resolve(completed)), 1);
|
|
8556
|
+
if (inFlight.size >= concurrency) {
|
|
8557
|
+
await drainOne();
|
|
8542
8558
|
}
|
|
8543
|
-
|
|
8544
|
-
uploadQueue.push(uploadPromise);
|
|
8559
|
+
inFlight.set(currentPartNumber, uploadPart(partData, currentPartNumber));
|
|
8545
8560
|
loaded += partData.byteLength;
|
|
8546
8561
|
if (onProgress) {
|
|
8547
8562
|
onProgress({
|
|
@@ -8555,14 +8570,15 @@ class S3Client {
|
|
|
8555
8570
|
if (done)
|
|
8556
8571
|
break;
|
|
8557
8572
|
}
|
|
8558
|
-
|
|
8559
|
-
|
|
8573
|
+
while (inFlight.size > 0) {
|
|
8574
|
+
await drainOne();
|
|
8575
|
+
}
|
|
8560
8576
|
parts.sort((a, b) => a.partNumber - b.partNumber);
|
|
8561
8577
|
return parts;
|
|
8562
8578
|
}
|
|
8563
|
-
async completeMultipartUpload(bucket, key, uploadId, parts) {
|
|
8579
|
+
async completeMultipartUpload(bucket, key, baseUrl, uploadId, parts) {
|
|
8564
8580
|
const credentials = await this.getCredentials();
|
|
8565
|
-
const url = `${
|
|
8581
|
+
const url = `${baseUrl}?uploadId=${encodeURIComponent(uploadId)}`;
|
|
8566
8582
|
const partsXml = parts.map((p) => `<Part><PartNumber>${p.partNumber}</PartNumber><ETag>"${p.etag}"</ETag></Part>`).join("");
|
|
8567
8583
|
const body = `<?xml version="1.0" encoding="UTF-8"?><CompleteMultipartUpload>${partsXml}</CompleteMultipartUpload>`;
|
|
8568
8584
|
const signed = signRequest({
|
|
@@ -8588,9 +8604,9 @@ class S3Client {
|
|
|
8588
8604
|
const etag = etagMatch ? etagMatch[1] : "";
|
|
8589
8605
|
return { etag };
|
|
8590
8606
|
}
|
|
8591
|
-
async abortMultipartUpload(bucket, key, uploadId) {
|
|
8607
|
+
async abortMultipartUpload(bucket, key, uploadId, baseUrl) {
|
|
8592
8608
|
const credentials = await this.getCredentials();
|
|
8593
|
-
const url = `${this.buildUrl(bucket, key)}?uploadId=${encodeURIComponent(uploadId)}`;
|
|
8609
|
+
const url = `${baseUrl ?? this.buildUrl(bucket, key)}?uploadId=${encodeURIComponent(uploadId)}`;
|
|
8594
8610
|
const signed = signRequest({
|
|
8595
8611
|
method: "DELETE",
|
|
8596
8612
|
url,
|
|
@@ -84074,6 +84090,31 @@ function resolveNginxSnippet(nginx, templates) {
|
|
|
84074
84090
|
out.push(...nginx.serverSnippet);
|
|
84075
84091
|
return out;
|
|
84076
84092
|
}
|
|
84093
|
+
function hstsHeader(hsts) {
|
|
84094
|
+
if (!hsts)
|
|
84095
|
+
return "";
|
|
84096
|
+
const o = typeof hsts === "object" ? hsts : {};
|
|
84097
|
+
const maxAge = o.maxAge ?? 31536000;
|
|
84098
|
+
const parts = [`max-age=${maxAge}`];
|
|
84099
|
+
if (o.includeSubDomains ?? true)
|
|
84100
|
+
parts.push("includeSubDomains");
|
|
84101
|
+
if (o.preload)
|
|
84102
|
+
parts.push("preload");
|
|
84103
|
+
return ` add_header Strict-Transport-Security "${parts.join("; ")}" always;`;
|
|
84104
|
+
}
|
|
84105
|
+
function securityRules(security) {
|
|
84106
|
+
if (!security || !security.allow?.length && !security.deny?.length)
|
|
84107
|
+
return [];
|
|
84108
|
+
const lines = [];
|
|
84109
|
+
for (const ip of security.deny || [])
|
|
84110
|
+
lines.push(` deny ${ip};`);
|
|
84111
|
+
if (security.allow?.length) {
|
|
84112
|
+
for (const ip of security.allow)
|
|
84113
|
+
lines.push(` allow ${ip};`);
|
|
84114
|
+
lines.push(" deny all;");
|
|
84115
|
+
}
|
|
84116
|
+
return lines;
|
|
84117
|
+
}
|
|
84077
84118
|
function htpasswdPath(siteName) {
|
|
84078
84119
|
return `/etc/nginx/.htpasswd-${siteName}`;
|
|
84079
84120
|
}
|
|
@@ -84082,7 +84123,7 @@ function isPhpSiteType(type) {
|
|
|
84082
84123
|
return PHP_TYPES.has(type);
|
|
84083
84124
|
}
|
|
84084
84125
|
function defaultWebDirectory(type) {
|
|
84085
|
-
return type === "laravel" || type === "statamic"
|
|
84126
|
+
return type === "laravel" || type === "statamic" ? "public" : "";
|
|
84086
84127
|
}
|
|
84087
84128
|
function resolveRoot(appDir, webDirectory) {
|
|
84088
84129
|
const base = appDir.replace(/\/+$/, "");
|
|
@@ -84099,13 +84140,14 @@ function vhostBody(options) {
|
|
|
84099
84140
|
` root ${root};`,
|
|
84100
84141
|
"",
|
|
84101
84142
|
' add_header X-Frame-Options "SAMEORIGIN";',
|
|
84102
|
-
' add_header X-Content-Type-Options "nosniff";'
|
|
84103
|
-
"",
|
|
84104
|
-
` index ${isPhp ? "index.php index.html" : "index.html index.htm"};`,
|
|
84105
|
-
"",
|
|
84106
|
-
" charset utf-8;",
|
|
84107
|
-
""
|
|
84143
|
+
' add_header X-Content-Type-Options "nosniff";'
|
|
84108
84144
|
];
|
|
84145
|
+
const hsts = hstsHeader(options.hsts);
|
|
84146
|
+
if (hsts)
|
|
84147
|
+
lines.push(hsts);
|
|
84148
|
+
for (const rule of securityRules(options.security))
|
|
84149
|
+
lines.push(rule);
|
|
84150
|
+
lines.push("", ` index ${isPhp ? "index.php index.html" : "index.html index.htm"};`, "", " charset utf-8;", "");
|
|
84109
84151
|
if (options.clientMaxBodySize) {
|
|
84110
84152
|
lines.push(` client_max_body_size ${options.clientMaxBodySize};`, "");
|
|
84111
84153
|
}
|
|
@@ -84118,7 +84160,10 @@ function vhostBody(options) {
|
|
|
84118
84160
|
if (Object.keys(options.redirects || {}).length > 0)
|
|
84119
84161
|
lines.push("");
|
|
84120
84162
|
if (isPhp) {
|
|
84121
|
-
lines.push(" location / {", " try_files $uri $uri/ /index.php?$query_string;", " }", "", " location = /favicon.ico { access_log off; log_not_found off; }", " location = /robots.txt { access_log off; log_not_found off; }", "", " error_page 404 /index.php;", "", " location ~ \\.php$ {", ` fastcgi_pass ${phpFpmSocketPath(phpVersion)};`, " fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;", " include fastcgi_params;", " }", "", " location ~ /\\.(?!well-known).* {", " deny all;", " }");
|
|
84163
|
+
lines.push(" location / {", " try_files $uri $uri/ /index.php?$query_string;", " }", "", " location = /favicon.ico { access_log off; log_not_found off; }", " location = /robots.txt { access_log off; log_not_found off; }", "", " error_page 404 /index.php;", "", " location ~ \\.php$ {", ` fastcgi_pass ${options.fastcgiPass ?? phpFpmSocketPath(phpVersion)};`, " fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;", " include fastcgi_params;", " }", "", " location ~ /\\.(?!well-known).* {", " deny all;", " }");
|
|
84164
|
+
if (type === "wordpress") {
|
|
84165
|
+
lines.push("", " location = /xmlrpc.php { deny all; }", " location ~* /(?:wp-config\\.php|readme\\.html|license\\.txt)$ { deny all; }", " location ~* /wp-content/uploads/.*\\.php$ { deny all; }");
|
|
84166
|
+
}
|
|
84122
84167
|
} else if (type === "spa") {
|
|
84123
84168
|
lines.push(" location / {", " try_files $uri $uri/ /index.html;", " }");
|
|
84124
84169
|
} else {
|
|
@@ -84150,6 +84195,7 @@ function buildNginxVhost(options) {
|
|
|
84150
84195
|
` server_name ${serverNames};`,
|
|
84151
84196
|
` ssl_certificate ${options.ssl.certPath};`,
|
|
84152
84197
|
` ssl_certificate_key ${options.ssl.keyPath};`,
|
|
84198
|
+
...options.tlsProtocols?.length ? [` ssl_protocols ${options.tlsProtocols.join(" ")};`] : [],
|
|
84153
84199
|
"",
|
|
84154
84200
|
...body,
|
|
84155
84201
|
"}"
|
|
@@ -84323,22 +84369,71 @@ function buildAutoUpdatesScript(enabled2 = true) {
|
|
|
84323
84369
|
|
|
84324
84370
|
// src/drivers/shared/monitoring.ts
|
|
84325
84371
|
var METRICS_PATH = "/var/lib/ts-cloud/metrics.json";
|
|
84326
|
-
|
|
84372
|
+
var ALERT_STATE_PATH = "/var/lib/ts-cloud/alert-state";
|
|
84373
|
+
var DEFAULT_CPU_LOAD_PER_CORE = 2;
|
|
84374
|
+
var DEFAULT_MEM_PERCENT = 90;
|
|
84375
|
+
var DEFAULT_DISK_PERCENT = 90;
|
|
84376
|
+
var SERVICE_PROBES = [
|
|
84377
|
+
["nginx", 80],
|
|
84378
|
+
["phpFpm", 9074],
|
|
84379
|
+
["mysql", 3306],
|
|
84380
|
+
["postgres", 5432],
|
|
84381
|
+
["redis", 6379],
|
|
84382
|
+
["meilisearch", 7700]
|
|
84383
|
+
];
|
|
84384
|
+
function resolveMonitoring(monitoring = true) {
|
|
84385
|
+
const obj = typeof monitoring === "object" ? monitoring : {};
|
|
84386
|
+
const enabled2 = typeof monitoring === "boolean" ? monitoring : monitoring.enabled !== false;
|
|
84387
|
+
return {
|
|
84388
|
+
enabled: enabled2,
|
|
84389
|
+
cpuLoadPerCore: obj.alerts?.cpuLoadPerCore ?? DEFAULT_CPU_LOAD_PER_CORE,
|
|
84390
|
+
memPercent: obj.alerts?.memPercent ?? DEFAULT_MEM_PERCENT,
|
|
84391
|
+
diskPercent: obj.alerts?.diskPercent ?? DEFAULT_DISK_PERCENT
|
|
84392
|
+
};
|
|
84393
|
+
}
|
|
84394
|
+
function buildMonitoringScript(monitoring = true) {
|
|
84395
|
+
const { enabled: enabled2, cpuLoadPerCore, memPercent, diskPercent } = resolveMonitoring(monitoring);
|
|
84327
84396
|
if (!enabled2)
|
|
84328
84397
|
return [];
|
|
84398
|
+
const probeLines = SERVICE_PROBES.map(([name, port]) => `SVC_${name.toUpperCase()}=$(probe ${port})`);
|
|
84399
|
+
const servicesJson = SERVICE_PROBES.map(([name]) => `"${name}":"$SVC_${name.toUpperCase()}"`).join(",");
|
|
84329
84400
|
return [
|
|
84330
84401
|
"mkdir -p /var/lib/ts-cloud",
|
|
84331
84402
|
"cat > /usr/local/bin/ts-cloud-metrics.sh <<'TS_CLOUD_METRICS_EOF'",
|
|
84332
84403
|
"#!/bin/bash",
|
|
84333
|
-
"set -
|
|
84404
|
+
"set -uo pipefail",
|
|
84334
84405
|
"LOAD=$(cut -d' ' -f1 /proc/loadavg)",
|
|
84406
|
+
"CPUS=$(nproc)",
|
|
84335
84407
|
"MEM_TOTAL=$(free -m | awk '/^Mem:/{print $2}')",
|
|
84336
84408
|
"MEM_USED=$(free -m | awk '/^Mem:/{print $3}')",
|
|
84409
|
+
"SWAP_TOTAL=$(free -m | awk '/^Swap:/{print $2}')",
|
|
84410
|
+
"SWAP_USED=$(free -m | awk '/^Swap:/{print $3}')",
|
|
84337
84411
|
`DISK_PCT=$(df -P / | awk 'NR==2{gsub("%","",$5); print $5}')`,
|
|
84338
|
-
"
|
|
84339
|
-
|
|
84340
|
-
'
|
|
84412
|
+
"UPTIME_SEC=$(cut -d' ' -f1 /proc/uptime | cut -d. -f1)",
|
|
84413
|
+
`RX_BYTES=$(awk -F'[: ]+' 'NR>2 && $2!="lo"{rx+=$3} END{print rx+0}' /proc/net/dev)`,
|
|
84414
|
+
`TX_BYTES=$(awk -F'[: ]+' 'NR>2 && $2!="lo"{tx+=$11} END{print tx+0}' /proc/net/dev)`,
|
|
84415
|
+
"probe(){ (exec 3<>/dev/tcp/127.0.0.1/$1) 2>/dev/null && echo up || echo down; }",
|
|
84416
|
+
...probeLines,
|
|
84417
|
+
"LOAD=${LOAD:-0}; CPUS=${CPUS:-1}; MEM_TOTAL=${MEM_TOTAL:-0}; MEM_USED=${MEM_USED:-0}",
|
|
84418
|
+
"SWAP_TOTAL=${SWAP_TOTAL:-0}; SWAP_USED=${SWAP_USED:-0}; DISK_PCT=${DISK_PCT:-0}",
|
|
84419
|
+
"UPTIME_SEC=${UPTIME_SEC:-0}; RX_BYTES=${RX_BYTES:-0}; TX_BYTES=${TX_BYTES:-0}",
|
|
84420
|
+
"MEM_PCT=$(( MEM_TOTAL > 0 ? MEM_USED * 100 / MEM_TOTAL : 0 ))",
|
|
84421
|
+
`cat > ${METRICS_PATH}.tmp <<JSON`,
|
|
84422
|
+
'{"load":$LOAD,"cpus":$CPUS,"memTotalMb":$MEM_TOTAL,"memUsedMb":$MEM_USED,"memUsedPct":$MEM_PCT,"swapTotalMb":$SWAP_TOTAL,"swapUsedMb":$SWAP_USED,"diskUsedPct":$DISK_PCT,"uptimeSec":$UPTIME_SEC,"network":{"rxBytes":$RX_BYTES,"txBytes":$TX_BYTES},"services":{' + servicesJson + "}}",
|
|
84341
84423
|
"JSON",
|
|
84424
|
+
`mv -f ${METRICS_PATH}.tmp ${METRICS_PATH}`,
|
|
84425
|
+
'ALERTS=""',
|
|
84426
|
+
`if awk -v l="$LOAD" -v c="$CPUS" -v t=${cpuLoadPerCore} 'BEGIN{exit !(c>0 && l/c > t)}'; then ALERTS="$ALERTS load=$LOAD/${cpuLoadPerCore}xCPU"; fi`,
|
|
84427
|
+
`if [ "\${MEM_PCT:-0}" -ge ${memPercent} ]; then ALERTS="$ALERTS mem=\${MEM_PCT}%"; fi`,
|
|
84428
|
+
`if [ "\${DISK_PCT:-0}" -ge ${diskPercent} ]; then ALERTS="$ALERTS disk=\${DISK_PCT}%"; fi`,
|
|
84429
|
+
`PREV=$(cat ${ALERT_STATE_PATH} 2>/dev/null || echo ok)`,
|
|
84430
|
+
'if [ -n "$ALERTS" ]; then',
|
|
84431
|
+
' if [ "$PREV" != alert ] && [ -x /usr/local/bin/ts-cloud-notify ]; then /usr/local/bin/ts-cloud-notify "⚠️ $(hostname): resource alert —$ALERTS" || true; fi',
|
|
84432
|
+
` echo alert > ${ALERT_STATE_PATH}`,
|
|
84433
|
+
"else",
|
|
84434
|
+
' if [ "$PREV" = alert ] && [ -x /usr/local/bin/ts-cloud-notify ]; then /usr/local/bin/ts-cloud-notify "✅ $(hostname): resource usage back to normal" || true; fi',
|
|
84435
|
+
` echo ok > ${ALERT_STATE_PATH}`,
|
|
84436
|
+
"fi",
|
|
84342
84437
|
"TS_CLOUD_METRICS_EOF",
|
|
84343
84438
|
"chmod +x /usr/local/bin/ts-cloud-metrics.sh",
|
|
84344
84439
|
"cat > /etc/systemd/system/ts-cloud-metrics.service <<'TS_CLOUD_METRICS_SVC_EOF'",
|
|
@@ -85379,6 +85474,10 @@ function buildRpxProvisionScript(options) {
|
|
|
85379
85474
|
const version2 = proxy.version ?? "latest";
|
|
85380
85475
|
const certsDir = config6.productionCerts.certsDir;
|
|
85381
85476
|
const launcher = renderRpxLauncher(config6);
|
|
85477
|
+
const upstreamTimeout = proxy.upstreamTimeout ?? 60;
|
|
85478
|
+
const poolEnv = [`Environment=RPX_UPSTREAM_TIMEOUT=${upstreamTimeout}`];
|
|
85479
|
+
if (typeof proxy.maxUpstreamConns === "number")
|
|
85480
|
+
poolEnv.push(`Environment=RPX_MAX_UPSTREAM_CONNS=${proxy.maxUpstreamConns}`);
|
|
85382
85481
|
return [
|
|
85383
85482
|
"set -euo pipefail",
|
|
85384
85483
|
`mkdir -p ${RPX_DIR} ${certsDir}`,
|
|
@@ -85394,6 +85493,7 @@ function buildRpxProvisionScript(options) {
|
|
|
85394
85493
|
"Type=simple",
|
|
85395
85494
|
`ExecStart=${bunBin} ${RPX_LAUNCHER_PATH}`,
|
|
85396
85495
|
`Environment=BUN_INSTALL=/root/.bun`,
|
|
85496
|
+
...poolEnv,
|
|
85397
85497
|
"Restart=always",
|
|
85398
85498
|
"RestartSec=5",
|
|
85399
85499
|
"LimitNOFILE=1048576",
|
|
@@ -86433,12 +86533,12 @@ function defaultDeployScriptFor(type) {
|
|
|
86433
86533
|
MACRO_RESTART_QUEUES
|
|
86434
86534
|
];
|
|
86435
86535
|
case "php":
|
|
86536
|
+
case "wordpress":
|
|
86436
86537
|
return [
|
|
86437
86538
|
MACRO_CREATE_RELEASE,
|
|
86438
86539
|
`${COMPOSER_INSTALL} || true`,
|
|
86439
86540
|
MACRO_ACTIVATE_RELEASE
|
|
86440
86541
|
];
|
|
86441
|
-
case "wordpress":
|
|
86442
86542
|
case "static":
|
|
86443
86543
|
case "spa":
|
|
86444
86544
|
return [
|
|
@@ -86474,6 +86574,19 @@ function buildCredentialFiles(releaseDir, creds) {
|
|
|
86474
86574
|
out.push(...writeFileHeredoc2(`${releaseDir}/.npmrc`, creds.npmrc, "TS_CLOUD_NPMRC_EOF"));
|
|
86475
86575
|
return out;
|
|
86476
86576
|
}
|
|
86577
|
+
function buildHealthCheckScript(site) {
|
|
86578
|
+
const path = site.healthCheck?.path;
|
|
86579
|
+
if (!path || !site.domain)
|
|
86580
|
+
return [];
|
|
86581
|
+
const p = path.startsWith("/") ? path : `/${path}`;
|
|
86582
|
+
const url = `http://127.0.0.1${p}`;
|
|
86583
|
+
return [
|
|
86584
|
+
`echo "[ts-cloud] health check ${url} (Host: ${site.domain})"`,
|
|
86585
|
+
"TS_CLOUD_HC_OK=0",
|
|
86586
|
+
`for i in $(seq 1 10); do TS_CLOUD_HC=$(curl -s -o /dev/null -w "%{http_code}" -H "Host: ${site.domain}" -m 10 "${url}" 2>/dev/null || echo 000); case "$TS_CLOUD_HC" in 2*|3*) TS_CLOUD_HC_OK=1; break ;; esac; sleep 3; done`,
|
|
86587
|
+
'[ "$TS_CLOUD_HC_OK" = 1 ] && echo "health check passed ($TS_CLOUD_HC)" || { echo "health check FAILED (last=$TS_CLOUD_HC)" >&2; exit 1; }'
|
|
86588
|
+
];
|
|
86589
|
+
}
|
|
86477
86590
|
function buildLaravelDeployScript(options) {
|
|
86478
86591
|
const { siteName, site, releaseId, commit } = options;
|
|
86479
86592
|
if (!site.repository?.url)
|
|
@@ -86655,6 +86768,68 @@ function siteHasServices(site) {
|
|
|
86655
86768
|
return !!(site.queues?.length || site.daemons?.length || site.scheduler);
|
|
86656
86769
|
}
|
|
86657
86770
|
|
|
86771
|
+
// src/drivers/shared/php-fpm-pool.ts
|
|
86772
|
+
var PHP_FPM_POOL_DIR = "/var/lib/pantry/php-fpm/pool.d";
|
|
86773
|
+
var POOL_PORT_BASE = 9100;
|
|
86774
|
+
var POOL_PORT_SPAN = 400;
|
|
86775
|
+
function siteToken(siteName) {
|
|
86776
|
+
const base = siteName.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
|
|
86777
|
+
return (base || "site").slice(0, 28);
|
|
86778
|
+
}
|
|
86779
|
+
function siteUser(siteName) {
|
|
86780
|
+
return `web_${siteToken(siteName)}`;
|
|
86781
|
+
}
|
|
86782
|
+
function phpFpmPoolPort(siteName) {
|
|
86783
|
+
const token = siteToken(siteName);
|
|
86784
|
+
let h = 0;
|
|
86785
|
+
for (let i = 0;i < token.length; i++)
|
|
86786
|
+
h = h * 31 + token.charCodeAt(i) >>> 0;
|
|
86787
|
+
return POOL_PORT_BASE + h % POOL_PORT_SPAN;
|
|
86788
|
+
}
|
|
86789
|
+
function phpFpmPoolListen(siteName) {
|
|
86790
|
+
return `127.0.0.1:${phpFpmPoolPort(siteName)}`;
|
|
86791
|
+
}
|
|
86792
|
+
function buildPhpFpmPoolConf(options) {
|
|
86793
|
+
const pool = siteToken(options.siteName);
|
|
86794
|
+
const user = siteUser(options.siteName);
|
|
86795
|
+
const maxChildren = options.maxChildren ?? 10;
|
|
86796
|
+
return [
|
|
86797
|
+
`; ts-cloud-managed pool for ${options.siteName} (site isolation)`,
|
|
86798
|
+
`[${pool}]`,
|
|
86799
|
+
`user = ${user}`,
|
|
86800
|
+
`group = ${user}`,
|
|
86801
|
+
`listen = ${phpFpmPoolListen(options.siteName)}`,
|
|
86802
|
+
"pm = dynamic",
|
|
86803
|
+
`pm.max_children = ${maxChildren}`,
|
|
86804
|
+
"pm.start_servers = 2",
|
|
86805
|
+
"pm.min_spare_servers = 1",
|
|
86806
|
+
"pm.max_spare_servers = 3",
|
|
86807
|
+
"pm.max_requests = 500",
|
|
86808
|
+
"catch_workers_output = yes",
|
|
86809
|
+
`php_admin_value[open_basedir] = ${options.appBase}:/tmp`,
|
|
86810
|
+
""
|
|
86811
|
+
].join(`
|
|
86812
|
+
`);
|
|
86813
|
+
}
|
|
86814
|
+
function buildPhpFpmPoolScript(options) {
|
|
86815
|
+
const user = siteUser(options.siteName);
|
|
86816
|
+
const pool = siteToken(options.siteName);
|
|
86817
|
+
const conf = `${PHP_FPM_POOL_DIR}/${pool}.conf`;
|
|
86818
|
+
return [
|
|
86819
|
+
`getent group ${user} >/dev/null || groupadd --system ${user}`,
|
|
86820
|
+
`id -u ${user} >/dev/null 2>&1 || useradd --system --no-create-home --shell /usr/sbin/nologin -g ${user} ${user}`,
|
|
86821
|
+
`usermod -aG ${user} www-data 2>/dev/null || true`,
|
|
86822
|
+
`chown -R ${user}:${user} ${options.appBase} || true`,
|
|
86823
|
+
`chmod -R g+rX ${options.appBase} || true`,
|
|
86824
|
+
`chmod 600 ${options.appBase}/shared/.env 2>/dev/null || true`,
|
|
86825
|
+
`mkdir -p ${PHP_FPM_POOL_DIR}`,
|
|
86826
|
+
`cat > ${conf} <<'TS_CLOUD_FPMPOOL_EOF'`,
|
|
86827
|
+
buildPhpFpmPoolConf(options).replace(/\n$/, ""),
|
|
86828
|
+
"TS_CLOUD_FPMPOOL_EOF",
|
|
86829
|
+
`(cd ${PANTRY_PROJECT_DIR} && pantry restart php-fpm) 2>/dev/null || true`
|
|
86830
|
+
];
|
|
86831
|
+
}
|
|
86832
|
+
|
|
86658
86833
|
// src/drivers/shared/compute-deploy.ts
|
|
86659
86834
|
var noopLogger = {
|
|
86660
86835
|
info: () => {},
|
|
@@ -86702,6 +86877,7 @@ async function deploySiteRelease(driver, options, logger4 = noopLogger) {
|
|
|
86702
86877
|
const useNginx = compute2?.webServer !== "rpx";
|
|
86703
86878
|
const sslProvider = resolveSslProvider(site);
|
|
86704
86879
|
const customCert = sslProvider === "custom" && site.ssl?.certPath && site.ssl?.keyPath ? { certPath: site.ssl.certPath, keyPath: site.ssl.keyPath } : undefined;
|
|
86880
|
+
const poolScript = site.isolation ? buildPhpFpmPoolScript({ siteName, appBase }) : [];
|
|
86705
86881
|
const vhostScript = useNginx ? buildNginxVhostScript({
|
|
86706
86882
|
siteName,
|
|
86707
86883
|
domain: site.domain || siteName,
|
|
@@ -86710,18 +86886,23 @@ async function deploySiteRelease(driver, options, logger4 = noopLogger) {
|
|
|
86710
86886
|
appDir: `${appBase}/current`,
|
|
86711
86887
|
webDirectory: site.webDirectory,
|
|
86712
86888
|
phpVersion,
|
|
86889
|
+
fastcgiPass: site.isolation ? phpFpmPoolListen(siteName) : undefined,
|
|
86713
86890
|
redirects: site.redirects,
|
|
86714
86891
|
ssl: customCert,
|
|
86715
86892
|
auth: site.auth && site.auth.enabled !== false && site.auth.password ? { username: site.auth.username || "admin", password: site.auth.password, realm: site.auth.realm } : undefined,
|
|
86716
86893
|
serverSnippet: resolveNginxSnippet(site.nginx, compute2?.nginxTemplates),
|
|
86717
|
-
clientMaxBodySize: site.nginx?.clientMaxBodySize
|
|
86894
|
+
clientMaxBodySize: site.nginx?.clientMaxBodySize,
|
|
86895
|
+
hsts: site.ssl?.hsts,
|
|
86896
|
+
tlsProtocols: site.ssl?.tlsProtocols,
|
|
86897
|
+
security: site.security
|
|
86718
86898
|
}) : [];
|
|
86719
86899
|
const sslScript = useNginx ? buildSslScript(site) : [];
|
|
86720
86900
|
const servicesScript = siteHasServices(site) ? buildSiteServicesScript({ slug, siteName, site, phpVersion, appBase }) : [];
|
|
86901
|
+
const healthCheckScript = useNginx ? buildHealthCheckScript(site) : [];
|
|
86721
86902
|
logger4.step(`Deploying PHP site '${siteName}' to ${targets.length} target(s)...`);
|
|
86722
86903
|
const phpResult = await driver.runRemoteDeploy({
|
|
86723
86904
|
targets,
|
|
86724
|
-
commands: [...deployScript, ...vhostScript, ...sslScript, ...servicesScript],
|
|
86905
|
+
commands: [...deployScript, ...poolScript, ...vhostScript, ...sslScript, ...servicesScript, ...healthCheckScript],
|
|
86725
86906
|
comment: `ts-cloud deploy ${slug}/${siteName}@${sha}`,
|
|
86726
86907
|
tags: { Project: slug, Environment: environment, Role: "app" }
|
|
86727
86908
|
});
|
|
@@ -86779,7 +86960,10 @@ async function deploySiteRelease(driver, options, logger4 = noopLogger) {
|
|
|
86779
86960
|
redirects: site.redirects,
|
|
86780
86961
|
auth: site.auth && site.auth.enabled !== false && site.auth.password ? { username: site.auth.username || "admin", password: site.auth.password, realm: site.auth.realm } : undefined,
|
|
86781
86962
|
serverSnippet: resolveNginxSnippet(site.nginx, compute?.nginxTemplates),
|
|
86782
|
-
clientMaxBodySize: site.nginx?.clientMaxBodySize
|
|
86963
|
+
clientMaxBodySize: site.nginx?.clientMaxBodySize,
|
|
86964
|
+
hsts: site.ssl?.hsts,
|
|
86965
|
+
tlsProtocols: site.ssl?.tlsProtocols,
|
|
86966
|
+
security: site.security
|
|
86783
86967
|
}) : [];
|
|
86784
86968
|
const staticSsl = wantsNginxStatic ? buildSslScript(site) : [];
|
|
86785
86969
|
const remoteScript = [...baseScript, ...staticVhost, ...staticSsl];
|
|
@@ -67,7 +67,7 @@ export declare class PreDeployScanner {
|
|
|
67
67
|
*/
|
|
68
68
|
private isLikelyPlaceholder;
|
|
69
69
|
/**
|
|
70
|
-
* Heuristic for
|
|
70
|
+
* Heuristic for `this match has way too little entropy to be a real secret`.
|
|
71
71
|
* Real cryptographic keys spread their characters across the alphabet; ASCII
|
|
72
72
|
* art dividers, padding placeholders, and runs of repeated chars do not.
|
|
73
73
|
*
|