@stacksjs/ts-cloud 0.2.21 → 0.2.22
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 +421 -403
- package/dist/deploy/index.d.ts +1 -0
- package/dist/deploy/site-target.d.ts +41 -0
- package/dist/drivers/hetzner/client.d.ts +5 -0
- package/dist/drivers/hetzner/driver.d.ts +56 -0
- package/dist/drivers/index.d.ts +2 -2
- package/dist/drivers/shared/caddyfile.d.ts +41 -1
- package/dist/drivers/shared/compute-deploy.d.ts +3 -1
- package/dist/drivers/shared/deploy-script.d.ts +20 -0
- package/dist/index.d.ts +3 -3
- package/dist/index.js +420 -50
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -80931,6 +80931,77 @@ class AcmeClient {
|
|
|
80931
80931
|
return this.accountKey;
|
|
80932
80932
|
}
|
|
80933
80933
|
}
|
|
80934
|
+
// src/deploy/site-target.ts
|
|
80935
|
+
function resolveSiteDeployTarget(site) {
|
|
80936
|
+
if (site.deploy)
|
|
80937
|
+
return site.deploy;
|
|
80938
|
+
if (site.start)
|
|
80939
|
+
return "server";
|
|
80940
|
+
return "bucket";
|
|
80941
|
+
}
|
|
80942
|
+
function resolveSiteKind(site) {
|
|
80943
|
+
const target = resolveSiteDeployTarget(site);
|
|
80944
|
+
if (target === "bucket")
|
|
80945
|
+
return "bucket";
|
|
80946
|
+
return site.start ? "server-app" : "server-static";
|
|
80947
|
+
}
|
|
80948
|
+
function hasComputeConfigured(config6) {
|
|
80949
|
+
return config6.infrastructure?.compute != null;
|
|
80950
|
+
}
|
|
80951
|
+
function validateDeploymentConfig(config6) {
|
|
80952
|
+
const errors = [];
|
|
80953
|
+
const warnings = [];
|
|
80954
|
+
const sites = config6.sites || {};
|
|
80955
|
+
const computeConfigured = hasComputeConfigured(config6);
|
|
80956
|
+
const portOwners = new Map;
|
|
80957
|
+
for (const [name, site] of Object.entries(sites)) {
|
|
80958
|
+
if (!site) {
|
|
80959
|
+
continue;
|
|
80960
|
+
}
|
|
80961
|
+
const target = resolveSiteDeployTarget(site);
|
|
80962
|
+
const kind = resolveSiteKind(site);
|
|
80963
|
+
if (target === "server" && !site.start && !site.root) {
|
|
80964
|
+
errors.push(`Site '${name}' sets deploy:'server' but declares neither \`start\` (dynamic app) nor \`root\` (static site to serve). Add one.`);
|
|
80965
|
+
continue;
|
|
80966
|
+
}
|
|
80967
|
+
if (kind === "server-app") {
|
|
80968
|
+
if (!computeConfigured) {
|
|
80969
|
+
errors.push(`Site '${name}' deploys to a server (deploy:'server'${site.deploy ? "" : " inferred from `start`"}) but no \`infrastructure.compute\` is configured. Set deploy:'bucket' or add a server (infrastructure.compute).`);
|
|
80970
|
+
}
|
|
80971
|
+
if (typeof site.port === "number") {
|
|
80972
|
+
const existing = portOwners.get(site.port);
|
|
80973
|
+
if (existing) {
|
|
80974
|
+
errors.push(`Sites '${existing}' and '${name}' both use port ${site.port}. Server apps sharing a box must use distinct ports.`);
|
|
80975
|
+
} else {
|
|
80976
|
+
portOwners.set(site.port, name);
|
|
80977
|
+
}
|
|
80978
|
+
}
|
|
80979
|
+
} else if (kind === "server-static") {
|
|
80980
|
+
if (!site.root) {
|
|
80981
|
+
errors.push(`Site '${name}' is a server static site (deploy:'server', no \`start\`) but has no \`root\` directory to serve.`);
|
|
80982
|
+
}
|
|
80983
|
+
if (!computeConfigured) {
|
|
80984
|
+
errors.push(`Site '${name}' deploys to a server (deploy:'server') but no \`infrastructure.compute\` is configured. Set deploy:'bucket' or add a server (infrastructure.compute).`);
|
|
80985
|
+
}
|
|
80986
|
+
} else {
|
|
80987
|
+
if (!site.root) {
|
|
80988
|
+
errors.push(`Site '${name}' deploys to a bucket but has no \`root\` directory to upload.`);
|
|
80989
|
+
}
|
|
80990
|
+
const serverOnly = [];
|
|
80991
|
+
if (site.start)
|
|
80992
|
+
serverOnly.push("start");
|
|
80993
|
+
if (typeof site.port === "number")
|
|
80994
|
+
serverOnly.push("port");
|
|
80995
|
+
if (site.preStart && site.preStart.length > 0)
|
|
80996
|
+
serverOnly.push("preStart");
|
|
80997
|
+
if (serverOnly.length > 0) {
|
|
80998
|
+
warnings.push(`Site '${name}' deploys to a bucket but sets server-only field(s): ${serverOnly.join(", ")}. These are ignored. Set deploy:'server' to use them.`);
|
|
80999
|
+
}
|
|
81000
|
+
}
|
|
81001
|
+
}
|
|
81002
|
+
return { errors, warnings };
|
|
81003
|
+
}
|
|
81004
|
+
|
|
80934
81005
|
// src/deploy/index.ts
|
|
80935
81006
|
init_static_site();
|
|
80936
81007
|
init_static_site_external_dns();
|
|
@@ -81159,46 +81230,202 @@ import { join as join13 } from "node:path";
|
|
|
81159
81230
|
import { execSync } from "node:child_process";
|
|
81160
81231
|
|
|
81161
81232
|
// src/drivers/shared/caddyfile.ts
|
|
81162
|
-
function
|
|
81163
|
-
|
|
81164
|
-
|
|
81165
|
-
|
|
81233
|
+
function isOnDemandDomain(domain) {
|
|
81234
|
+
return domain === "*" || domain.includes("*");
|
|
81235
|
+
}
|
|
81236
|
+
function isStaticApp(app) {
|
|
81237
|
+
return typeof app.root === "string" && app.root.length > 0;
|
|
81238
|
+
}
|
|
81239
|
+
function isProxyApp(app) {
|
|
81240
|
+
return typeof app.port === "number";
|
|
81241
|
+
}
|
|
81242
|
+
function staticSiteServerRoot(name) {
|
|
81243
|
+
return `/var/www/${name}`;
|
|
81244
|
+
}
|
|
81245
|
+
function normalizeOnDemandTls(onDemandTls) {
|
|
81246
|
+
if (!onDemandTls)
|
|
81247
|
+
return;
|
|
81248
|
+
if (onDemandTls === true)
|
|
81249
|
+
return {};
|
|
81250
|
+
return onDemandTls;
|
|
81251
|
+
}
|
|
81252
|
+
function wrapInHandle(body, path, indent) {
|
|
81253
|
+
const inner = body.split(`
|
|
81254
|
+
`).map((line) => ` ${line}`).join(`
|
|
81255
|
+
`);
|
|
81256
|
+
return `${indent}handle ${path} {
|
|
81257
|
+
${inner}
|
|
81258
|
+
${indent}}`;
|
|
81259
|
+
}
|
|
81260
|
+
function renderUpstreamBlock(app, indent) {
|
|
81261
|
+
const host = app.upstreamHost || "localhost";
|
|
81262
|
+
const upstream = `${host}:${app.port}`;
|
|
81263
|
+
const directives = app.reverseProxyDirectives ?? [];
|
|
81264
|
+
const proxyLine = directives.length === 0 ? `${indent}reverse_proxy ${upstream}` : [
|
|
81265
|
+
`${indent}reverse_proxy ${upstream} {`,
|
|
81266
|
+
...directives.map((d) => `${indent} ${d}`),
|
|
81267
|
+
`${indent}}`
|
|
81268
|
+
].join(`
|
|
81269
|
+
`);
|
|
81270
|
+
const isCatchAll = !app.path || app.path === "/";
|
|
81271
|
+
if (isCatchAll)
|
|
81272
|
+
return proxyLine;
|
|
81273
|
+
return wrapInHandle(proxyLine, app.path, indent);
|
|
81274
|
+
}
|
|
81275
|
+
function renderStaticBlock(app, indent) {
|
|
81276
|
+
const root = app.root;
|
|
81277
|
+
const lines = [`${indent}root * ${root}`];
|
|
81278
|
+
if (app.cache?.enabled) {
|
|
81279
|
+
const maxAge = app.cache.maxAge ?? 3600;
|
|
81280
|
+
lines.push(`${indent}header Cache-Control "public, max-age=${maxAge}"`);
|
|
81281
|
+
}
|
|
81282
|
+
if (app.spa) {
|
|
81283
|
+
lines.push(`${indent}try_files {path} /index.html`);
|
|
81284
|
+
} else if (app.pathRewriteStyle === "flat") {
|
|
81285
|
+
lines.push(`${indent}try_files {path} {path}.html {path}/index.html`);
|
|
81286
|
+
} else {
|
|
81287
|
+
lines.push(`${indent}try_files {path} {path}/index.html {path}.html`);
|
|
81288
|
+
}
|
|
81289
|
+
lines.push(`${indent}file_server`);
|
|
81290
|
+
const body = lines.join(`
|
|
81291
|
+
`);
|
|
81292
|
+
const isCatchAll = !app.path || app.path === "/";
|
|
81293
|
+
if (isCatchAll)
|
|
81294
|
+
return body;
|
|
81295
|
+
return wrapInHandle(body, app.path, indent);
|
|
81296
|
+
}
|
|
81297
|
+
function renderAppBlock(app, indent) {
|
|
81298
|
+
return isStaticApp(app) ? renderStaticBlock(app, indent) : renderUpstreamBlock(app, indent);
|
|
81299
|
+
}
|
|
81300
|
+
function groupAppsByDomains(apps) {
|
|
81301
|
+
const groups = new Map;
|
|
81302
|
+
for (const app of apps) {
|
|
81303
|
+
const domains = [...app.domains].filter(Boolean);
|
|
81304
|
+
if (domains.length === 0)
|
|
81305
|
+
continue;
|
|
81306
|
+
const key = [...domains].sort().join(" ");
|
|
81307
|
+
const group = groups.get(key) ?? { domains, apps: [] };
|
|
81308
|
+
group.apps.push(app);
|
|
81309
|
+
groups.set(key, group);
|
|
81310
|
+
}
|
|
81311
|
+
return [...groups.values()];
|
|
81312
|
+
}
|
|
81313
|
+
function sortAppsByPath(apps) {
|
|
81314
|
+
return [...apps].sort((a, b) => {
|
|
81315
|
+
const aCatchAll = !a.path || a.path === "/";
|
|
81316
|
+
const bCatchAll = !b.path || b.path === "/";
|
|
81317
|
+
if (aCatchAll && !bCatchAll)
|
|
81318
|
+
return 1;
|
|
81319
|
+
if (!aCatchAll && bCatchAll)
|
|
81320
|
+
return -1;
|
|
81321
|
+
return (b.path?.length ?? 0) - (a.path?.length ?? 0);
|
|
81322
|
+
});
|
|
81323
|
+
}
|
|
81324
|
+
function buildCaddyfileFromProxy(proxy) {
|
|
81325
|
+
if (proxy.raw && proxy.raw.trim())
|
|
81326
|
+
return proxy.raw.trim();
|
|
81327
|
+
const apps = (proxy.apps ?? []).filter((app) => app.domains.length > 0 && (isProxyApp(app) || isStaticApp(app)));
|
|
81328
|
+
if (apps.length === 0)
|
|
81166
81329
|
return;
|
|
81167
|
-
const
|
|
81168
|
-
|
|
81169
|
-
|
|
81170
|
-
|
|
81171
|
-
|
|
81330
|
+
const onDemand = normalizeOnDemandTls(proxy.onDemandTls);
|
|
81331
|
+
const hasOnDemandDomain = apps.some((app) => app.domains.some(isOnDemandDomain));
|
|
81332
|
+
const globalLines = [];
|
|
81333
|
+
if (proxy.email)
|
|
81334
|
+
globalLines.push(`email ${proxy.email}`);
|
|
81335
|
+
if (proxy.staging)
|
|
81336
|
+
globalLines.push("acme_ca https://acme-staging-v02.api.letsencrypt.org/directory");
|
|
81337
|
+
if (onDemand) {
|
|
81338
|
+
if (onDemand.ask || onDemand.interval || onDemand.burst != null) {
|
|
81339
|
+
const inner = [];
|
|
81340
|
+
if (onDemand.ask)
|
|
81341
|
+
inner.push(` ask ${onDemand.ask}`);
|
|
81342
|
+
if (onDemand.interval || onDemand.burst != null) {
|
|
81343
|
+
const rl = [];
|
|
81344
|
+
if (onDemand.interval)
|
|
81345
|
+
rl.push(` interval ${onDemand.interval}`);
|
|
81346
|
+
if (onDemand.burst != null)
|
|
81347
|
+
rl.push(` burst ${onDemand.burst}`);
|
|
81348
|
+
inner.push(` rate_limit {
|
|
81349
|
+
${rl.join(`
|
|
81350
|
+
`)}
|
|
81351
|
+
}`);
|
|
81352
|
+
}
|
|
81353
|
+
globalLines.push(`on_demand_tls {
|
|
81354
|
+
${inner.join(`
|
|
81355
|
+
`)}
|
|
81356
|
+
}`);
|
|
81357
|
+
} else {
|
|
81358
|
+
globalLines.push(`on_demand_tls {
|
|
81359
|
+
}`);
|
|
81360
|
+
}
|
|
81172
81361
|
}
|
|
81362
|
+
for (const directive of proxy.globalDirectives ?? [])
|
|
81363
|
+
globalLines.push(directive);
|
|
81173
81364
|
const blocks = [];
|
|
81174
|
-
|
|
81175
|
-
|
|
81176
|
-
|
|
81177
|
-
const bIsCatchAll = !b.path || b.path === "/";
|
|
81178
|
-
if (aIsCatchAll && !bIsCatchAll)
|
|
81179
|
-
return 1;
|
|
81180
|
-
if (!aIsCatchAll && bIsCatchAll)
|
|
81181
|
-
return -1;
|
|
81182
|
-
return (b.path?.length ?? 0) - (a.path?.length ?? 0);
|
|
81183
|
-
});
|
|
81184
|
-
const handles = sorted.map((s) => {
|
|
81185
|
-
const isCatchAll = !s.path || s.path === "/";
|
|
81186
|
-
const inner = `reverse_proxy localhost:${s.port}`;
|
|
81187
|
-
return isCatchAll ? ` handle {
|
|
81188
|
-
${inner}
|
|
81189
|
-
}` : ` handle ${s.path} {
|
|
81190
|
-
${inner}
|
|
81191
|
-
}`;
|
|
81192
|
-
});
|
|
81193
|
-
blocks.push(`${domain} {
|
|
81194
|
-
${handles.join(`
|
|
81365
|
+
if (globalLines.length > 0)
|
|
81366
|
+
blocks.push(`{
|
|
81367
|
+
${globalLines.map((line) => ` ${line}`).join(`
|
|
81195
81368
|
`)}
|
|
81196
81369
|
}`);
|
|
81370
|
+
for (const group of groupAppsByDomains(apps)) {
|
|
81371
|
+
const sorted = sortAppsByPath(group.apps);
|
|
81372
|
+
const body = sorted.map((app) => renderAppBlock(app, " ")).join(`
|
|
81373
|
+
`);
|
|
81374
|
+
const needsOnDemand = onDemand && group.domains.some(isOnDemandDomain);
|
|
81375
|
+
const tlsBlock = needsOnDemand ? `
|
|
81376
|
+
tls {
|
|
81377
|
+
on_demand
|
|
81378
|
+
}` : "";
|
|
81379
|
+
blocks.push(`${group.domains.join(", ")} {
|
|
81380
|
+
${body}${tlsBlock}
|
|
81381
|
+
}`);
|
|
81382
|
+
}
|
|
81383
|
+
if (hasOnDemandDomain && !onDemand) {
|
|
81384
|
+
blocks.unshift(`# WARNING: wildcard/catch-all domain present but on_demand_tls is not enabled.
|
|
81385
|
+
` + "# Caddy cannot provision TLS for these hosts. Set compute.proxy.onDemandTls.");
|
|
81197
81386
|
}
|
|
81198
81387
|
return blocks.join(`
|
|
81199
81388
|
|
|
81200
81389
|
`);
|
|
81201
81390
|
}
|
|
81391
|
+
function proxyConfigFromSites(sites) {
|
|
81392
|
+
const apps = [];
|
|
81393
|
+
for (const [name, site] of Object.entries(sites)) {
|
|
81394
|
+
if (typeof site.domain !== "string" || !site.domain)
|
|
81395
|
+
continue;
|
|
81396
|
+
if (typeof site.port === "number" && site.deploy !== "bucket") {
|
|
81397
|
+
apps.push({
|
|
81398
|
+
name,
|
|
81399
|
+
domains: [site.domain],
|
|
81400
|
+
port: site.port,
|
|
81401
|
+
path: site.path
|
|
81402
|
+
});
|
|
81403
|
+
} else if (resolveSiteKind(site) === "server-static") {
|
|
81404
|
+
apps.push({
|
|
81405
|
+
name,
|
|
81406
|
+
domains: [site.domain],
|
|
81407
|
+
root: staticSiteServerRoot(name),
|
|
81408
|
+
path: site.path,
|
|
81409
|
+
spa: site.spa,
|
|
81410
|
+
pathRewriteStyle: site.pathRewriteStyle,
|
|
81411
|
+
cache: site.cache
|
|
81412
|
+
});
|
|
81413
|
+
}
|
|
81414
|
+
}
|
|
81415
|
+
return { apps };
|
|
81416
|
+
}
|
|
81417
|
+
function resolveCaddyfile(sites, proxy) {
|
|
81418
|
+
if (proxy) {
|
|
81419
|
+
if (proxy.raw && proxy.raw.trim())
|
|
81420
|
+
return proxy.raw.trim();
|
|
81421
|
+
const resolved = proxy.apps && proxy.apps.length > 0 ? proxy : { ...proxy, apps: proxyConfigFromSites(sites).apps };
|
|
81422
|
+
return buildCaddyfileFromProxy(resolved);
|
|
81423
|
+
}
|
|
81424
|
+
return buildCaddyfileFromProxy(proxyConfigFromSites(sites));
|
|
81425
|
+
}
|
|
81426
|
+
function buildCaddyfile(sites) {
|
|
81427
|
+
return buildCaddyfileFromProxy(proxyConfigFromSites(sites));
|
|
81428
|
+
}
|
|
81202
81429
|
|
|
81203
81430
|
// src/drivers/hetzner/client.ts
|
|
81204
81431
|
var DEFAULT_API_URL = "https://api.hetzner.cloud/v1";
|
|
@@ -81223,10 +81450,20 @@ class HetznerClient {
|
|
|
81223
81450
|
body: body === undefined ? undefined : JSON.stringify(body)
|
|
81224
81451
|
});
|
|
81225
81452
|
const text = await response.text();
|
|
81226
|
-
|
|
81453
|
+
let data;
|
|
81454
|
+
try {
|
|
81455
|
+
data = text ? JSON.parse(text) : {};
|
|
81456
|
+
} catch {
|
|
81457
|
+
if (!response.ok) {
|
|
81458
|
+
const snippet = text.trim().slice(0, 200) || response.statusText || "Hetzner API error";
|
|
81459
|
+
throw new Error(`Hetzner API ${method} ${path} (${response.status}): ${snippet}`);
|
|
81460
|
+
}
|
|
81461
|
+
throw new Error(`Hetzner API ${method} ${path}: unexpected non-JSON response`);
|
|
81462
|
+
}
|
|
81227
81463
|
if (!response.ok) {
|
|
81228
81464
|
const message = data.error?.message || response.statusText || "Hetzner API error";
|
|
81229
|
-
|
|
81465
|
+
const code = data.error?.code ? ` [${data.error.code}]` : "";
|
|
81466
|
+
throw new Error(`Hetzner API ${method} ${path} (${response.status})${code}: ${message}`);
|
|
81230
81467
|
}
|
|
81231
81468
|
return data;
|
|
81232
81469
|
}
|
|
@@ -81270,6 +81507,12 @@ class HetznerClient {
|
|
|
81270
81507
|
});
|
|
81271
81508
|
return { firewall: data.firewall, actions: data.actions };
|
|
81272
81509
|
}
|
|
81510
|
+
async setFirewallRules(firewallId, rules) {
|
|
81511
|
+
const data = await this.request("POST", `/firewalls/${firewallId}/actions/set_rules`, {
|
|
81512
|
+
rules
|
|
81513
|
+
});
|
|
81514
|
+
return data.actions ?? [];
|
|
81515
|
+
}
|
|
81273
81516
|
async applyFirewallToResources(firewallId, applyTo) {
|
|
81274
81517
|
const data = await this.request("POST", `/firewalls/${firewallId}/actions/apply_to_resources`, {
|
|
81275
81518
|
apply_to: applyTo
|
|
@@ -81536,6 +81779,8 @@ class HetznerDriver {
|
|
|
81536
81779
|
sshPublicKeyPath;
|
|
81537
81780
|
sshUser;
|
|
81538
81781
|
location;
|
|
81782
|
+
waitForBoot;
|
|
81783
|
+
bootWait;
|
|
81539
81784
|
constructor(options = {}) {
|
|
81540
81785
|
this.client = options.client ?? new HetznerClient({
|
|
81541
81786
|
apiToken: resolveHetznerApiToken(options.apiToken)
|
|
@@ -81544,6 +81789,13 @@ class HetznerDriver {
|
|
|
81544
81789
|
this.sshPublicKeyPath = expandHome(options.sshPublicKeyPath || process.env.HCLOUD_SSH_PUBLIC_KEY || `${this.sshPrivateKeyPath}.pub`);
|
|
81545
81790
|
this.sshUser = options.sshUser || process.env.HCLOUD_SSH_USER || "root";
|
|
81546
81791
|
this.location = options.location || process.env.HCLOUD_LOCATION || "fsn1";
|
|
81792
|
+
this.waitForBoot = options.waitForBoot ?? true;
|
|
81793
|
+
this.bootWait = {
|
|
81794
|
+
sshIntervalMs: options.bootWait?.sshIntervalMs ?? 5000,
|
|
81795
|
+
sshTimeoutMs: options.bootWait?.sshTimeoutMs ?? 300000,
|
|
81796
|
+
cloudInitIntervalMs: options.bootWait?.cloudInitIntervalMs ?? 5000,
|
|
81797
|
+
cloudInitTimeoutMs: options.bootWait?.cloudInitTimeoutMs ?? 600000
|
|
81798
|
+
};
|
|
81547
81799
|
}
|
|
81548
81800
|
async provisionComputeInfrastructure(options) {
|
|
81549
81801
|
const { config: config6, environment } = options;
|
|
@@ -81553,16 +81805,32 @@ class HetznerDriver {
|
|
|
81553
81805
|
throw new Error("infrastructure.compute is required to provision Hetzner compute");
|
|
81554
81806
|
}
|
|
81555
81807
|
const stackName = resolveProjectStackName(config6, environment);
|
|
81808
|
+
const serverName = `${slug}-${environment}-app`;
|
|
81556
81809
|
const existing = await readDriverState(stackName);
|
|
81557
81810
|
if (existing?.serverId) {
|
|
81558
|
-
const server2 = await this.
|
|
81559
|
-
if (server2.status !== "off") {
|
|
81811
|
+
const server2 = await this.tryGetServer(existing.serverId);
|
|
81812
|
+
if (server2 && server2.status !== "off") {
|
|
81560
81813
|
return this.outputsFromState(existing, server2);
|
|
81561
81814
|
}
|
|
81562
81815
|
}
|
|
81816
|
+
const labels = tsCloudLabels(slug, environment, "app");
|
|
81817
|
+
const alreadyRunning = await this.findExistingServer(slug, environment, serverName);
|
|
81818
|
+
if (alreadyRunning && alreadyRunning.status !== "off") {
|
|
81819
|
+
const rehydrated = {
|
|
81820
|
+
provider: "hetzner",
|
|
81821
|
+
stackName,
|
|
81822
|
+
serverId: alreadyRunning.id,
|
|
81823
|
+
serverName: alreadyRunning.name,
|
|
81824
|
+
publicIp: alreadyRunning.public_net.ipv4?.ip,
|
|
81825
|
+
deployStoragePath: "/var/ts-cloud/staging",
|
|
81826
|
+
sshUser: this.sshUser
|
|
81827
|
+
};
|
|
81828
|
+
await writeDriverState(stackName, rehydrated);
|
|
81829
|
+
return this.outputsFromState(rehydrated, alreadyRunning);
|
|
81830
|
+
}
|
|
81563
81831
|
const sites = config6.sites || {};
|
|
81564
|
-
const
|
|
81565
|
-
const
|
|
81832
|
+
const caddyfile = resolveCaddyfile(sites, compute.proxy);
|
|
81833
|
+
const sitePorts = caddyfile ? [] : this.collectUpstreamPorts(sites, compute.proxy);
|
|
81566
81834
|
const bootstrap = generateUbuntuAppCloudInit({
|
|
81567
81835
|
runtime: compute.runtime || "bun",
|
|
81568
81836
|
runtimeVersion: compute.runtimeVersion || "latest",
|
|
@@ -81571,19 +81839,13 @@ class HetznerDriver {
|
|
|
81571
81839
|
caddyfile
|
|
81572
81840
|
});
|
|
81573
81841
|
const userData = wrapCloudInitUserData(bootstrap);
|
|
81574
|
-
const serverName = `${slug}-${environment}-app`;
|
|
81575
81842
|
const serverType = resolveHetznerServerType(compute.size);
|
|
81576
81843
|
const image = compute.image || config6.hetzner?.image || "ubuntu-24.04";
|
|
81577
|
-
const labels = tsCloudLabels(slug, environment, "app");
|
|
81578
81844
|
const firewallName = `${slug}-${environment}-app-fw`;
|
|
81579
|
-
const { firewall } = await this.
|
|
81580
|
-
|
|
81581
|
-
|
|
81582
|
-
|
|
81583
|
-
allowSsh: compute.allowSsh !== false,
|
|
81584
|
-
sitePorts
|
|
81585
|
-
})
|
|
81586
|
-
});
|
|
81845
|
+
const { firewall } = await this.ensureFirewall(firewallName, labels, buildHetznerFirewallRules({
|
|
81846
|
+
allowSsh: compute.allowSsh !== false,
|
|
81847
|
+
sitePorts
|
|
81848
|
+
}));
|
|
81587
81849
|
const sshKeyId = await this.ensureSshKey(slug, environment, labels);
|
|
81588
81850
|
const { server, action } = await this.client.createServer({
|
|
81589
81851
|
name: serverName,
|
|
@@ -81608,6 +81870,11 @@ class HetznerDriver {
|
|
|
81608
81870
|
sshUser: this.sshUser
|
|
81609
81871
|
};
|
|
81610
81872
|
await writeDriverState(stackName, state);
|
|
81873
|
+
const ip = running.public_net.ipv4?.ip;
|
|
81874
|
+
if (ip && this.waitForBoot) {
|
|
81875
|
+
await this.waitForSshReady(ip);
|
|
81876
|
+
await this.waitForCloudInit(ip);
|
|
81877
|
+
}
|
|
81611
81878
|
return this.outputsFromState(state, running);
|
|
81612
81879
|
}
|
|
81613
81880
|
async getComputeOutputs(options) {
|
|
@@ -81714,6 +81981,79 @@ class HetznerDriver {
|
|
|
81714
81981
|
});
|
|
81715
81982
|
return created.id;
|
|
81716
81983
|
}
|
|
81984
|
+
async tryGetServer(id) {
|
|
81985
|
+
try {
|
|
81986
|
+
return await this.client.getServer(id);
|
|
81987
|
+
} catch {
|
|
81988
|
+
return null;
|
|
81989
|
+
}
|
|
81990
|
+
}
|
|
81991
|
+
async findExistingServer(slug, environment, serverName) {
|
|
81992
|
+
const servers = await this.client.listServers();
|
|
81993
|
+
return servers.find((server) => matchesTsCloudLabels(server.labels, slug, environment, "app") || server.name === serverName);
|
|
81994
|
+
}
|
|
81995
|
+
async ensureFirewall(name, labels, rules) {
|
|
81996
|
+
const existing = await this.client.listFirewalls();
|
|
81997
|
+
const match = existing.find((fw) => fw.name === name);
|
|
81998
|
+
if (match) {
|
|
81999
|
+
await this.client.setFirewallRules(match.id, rules);
|
|
82000
|
+
return { firewall: match };
|
|
82001
|
+
}
|
|
82002
|
+
const { firewall } = await this.client.createFirewall({ name, labels, rules });
|
|
82003
|
+
return { firewall };
|
|
82004
|
+
}
|
|
82005
|
+
collectUpstreamPorts(sites, proxy) {
|
|
82006
|
+
const ports = new Set;
|
|
82007
|
+
for (const app of proxy?.apps ?? []) {
|
|
82008
|
+
if (typeof app.port === "number")
|
|
82009
|
+
ports.add(app.port);
|
|
82010
|
+
}
|
|
82011
|
+
for (const site of Object.values(sites)) {
|
|
82012
|
+
if (typeof site.port === "number")
|
|
82013
|
+
ports.add(site.port);
|
|
82014
|
+
}
|
|
82015
|
+
return [...ports].filter((port) => ![80, 443].includes(port));
|
|
82016
|
+
}
|
|
82017
|
+
async sleep(ms) {
|
|
82018
|
+
await new Promise((resolve13) => setTimeout(resolve13, ms));
|
|
82019
|
+
}
|
|
82020
|
+
async waitForSshReady(host) {
|
|
82021
|
+
const { sshIntervalMs, sshTimeoutMs } = this.bootWait;
|
|
82022
|
+
const start = Date.now();
|
|
82023
|
+
let lastErr;
|
|
82024
|
+
while (Date.now() - start < sshTimeoutMs) {
|
|
82025
|
+
try {
|
|
82026
|
+
execSync(`ssh ${this.sshBaseArgs(host, ["-o", "ConnectTimeout=5"]).map((a) => `"${a.replace(/"/g, "\\\"")}"`).join(" ")} true`, {
|
|
82027
|
+
stdio: "pipe",
|
|
82028
|
+
maxBuffer: SSH_MAX_BUFFER
|
|
82029
|
+
});
|
|
82030
|
+
return;
|
|
82031
|
+
} catch (err) {
|
|
82032
|
+
lastErr = err;
|
|
82033
|
+
await this.sleep(sshIntervalMs);
|
|
82034
|
+
}
|
|
82035
|
+
}
|
|
82036
|
+
throw new Error(`Timed out waiting for SSH on ${host} after ${sshTimeoutMs}ms: ${lastErr?.message ?? "unknown error"}`);
|
|
82037
|
+
}
|
|
82038
|
+
async waitForCloudInit(host) {
|
|
82039
|
+
const { cloudInitIntervalMs, cloudInitTimeoutMs } = this.bootWait;
|
|
82040
|
+
const start = Date.now();
|
|
82041
|
+
while (Date.now() - start < cloudInitTimeoutMs) {
|
|
82042
|
+
try {
|
|
82043
|
+
const out = this.sshExec(host, "cloud-init status --long 2>/dev/null || cloud-init status 2>/dev/null || echo status:\\ done");
|
|
82044
|
+
if (/status:\s*done/.test(out))
|
|
82045
|
+
return;
|
|
82046
|
+
if (/status:\s*error/.test(out))
|
|
82047
|
+
throw new Error(`cloud-init reported an error on ${host}:
|
|
82048
|
+
${out}`);
|
|
82049
|
+
} catch (err) {
|
|
82050
|
+
if (err instanceof Error && /cloud-init reported an error/.test(err.message))
|
|
82051
|
+
throw err;
|
|
82052
|
+
}
|
|
82053
|
+
await this.sleep(cloudInitIntervalMs);
|
|
82054
|
+
}
|
|
82055
|
+
throw new Error(`Timed out waiting for cloud-init to finish on ${host} after ${cloudInitTimeoutMs}ms`);
|
|
82056
|
+
}
|
|
81717
82057
|
outputsFromState(state, server) {
|
|
81718
82058
|
return {
|
|
81719
82059
|
deployStoragePath: state.deployStoragePath || "/var/ts-cloud/staging",
|
|
@@ -81722,7 +82062,7 @@ class HetznerDriver {
|
|
|
81722
82062
|
sshUser: state.sshUser || this.sshUser
|
|
81723
82063
|
};
|
|
81724
82064
|
}
|
|
81725
|
-
sshBaseArgs(host) {
|
|
82065
|
+
sshBaseArgs(host, extra = []) {
|
|
81726
82066
|
return [
|
|
81727
82067
|
"-i",
|
|
81728
82068
|
this.sshPrivateKeyPath,
|
|
@@ -81730,6 +82070,7 @@ class HetznerDriver {
|
|
|
81730
82070
|
"StrictHostKeyChecking=accept-new",
|
|
81731
82071
|
"-o",
|
|
81732
82072
|
"BatchMode=yes",
|
|
82073
|
+
...extra,
|
|
81733
82074
|
`${this.sshUser}@${host}`
|
|
81734
82075
|
];
|
|
81735
82076
|
}
|
|
@@ -81843,6 +82184,19 @@ function buildSiteDeployScript(options) {
|
|
|
81843
82184
|
`systemctl is-active ${serviceName}`
|
|
81844
82185
|
];
|
|
81845
82186
|
}
|
|
82187
|
+
function buildStaticSiteDeployScript(options) {
|
|
82188
|
+
const { siteName, artifactFetch, preStartCommands = [] } = options;
|
|
82189
|
+
const appDir = options.appDir ?? `/var/www/${siteName}`;
|
|
82190
|
+
const preStart = preStartCommands.length > 0 ? [`cd ${appDir}`, ...preStartCommands] : [];
|
|
82191
|
+
return [
|
|
82192
|
+
"set -euo pipefail",
|
|
82193
|
+
...artifactFetch,
|
|
82194
|
+
`mkdir -p ${appDir}`,
|
|
82195
|
+
`find ${appDir} -mindepth 1 -maxdepth 1 -exec rm -rf {} +`,
|
|
82196
|
+
`tar xzf /tmp/${siteName}-release.tar.gz -C ${appDir}`,
|
|
82197
|
+
...preStart
|
|
82198
|
+
];
|
|
82199
|
+
}
|
|
81846
82200
|
function buildAwsArtifactFetch(bucket, key, region, siteName) {
|
|
81847
82201
|
return [
|
|
81848
82202
|
`aws s3 cp "s3://${bucket}/${key}" /tmp/${siteName}-release.tar.gz --region ${region}`
|
|
@@ -81892,7 +82246,11 @@ async function deploySiteRelease(driver, options, logger4 = noopLogger) {
|
|
|
81892
82246
|
targets
|
|
81893
82247
|
});
|
|
81894
82248
|
const artifactFetch = driver.name === "aws" ? buildAwsArtifactFetch(outputs.deployBucketName, remoteKey, config6.project.region || "us-east-1", siteName) : buildLocalArtifactFetch(uploadResult.artifactRef, siteName);
|
|
81895
|
-
const remoteScript =
|
|
82249
|
+
const remoteScript = resolveSiteKind(site) === "server-static" ? buildStaticSiteDeployScript({
|
|
82250
|
+
siteName,
|
|
82251
|
+
artifactFetch,
|
|
82252
|
+
preStartCommands: site.preStart
|
|
82253
|
+
}) : buildSiteDeployScript({
|
|
81896
82254
|
siteName,
|
|
81897
82255
|
slug,
|
|
81898
82256
|
artifactFetch,
|
|
@@ -81931,8 +82289,11 @@ async function deployAllComputeSites(options) {
|
|
|
81931
82289
|
const slug = config6.project.slug;
|
|
81932
82290
|
const sites = config6.sites || {};
|
|
81933
82291
|
const deployable = Object.entries(sites).filter(([name, site]) => {
|
|
81934
|
-
if (!site
|
|
81935
|
-
|
|
82292
|
+
if (!site)
|
|
82293
|
+
return false;
|
|
82294
|
+
const kind = resolveSiteKind(site);
|
|
82295
|
+
if (kind === "bucket") {
|
|
82296
|
+
logger4.warn(`Site '${name}' targets a bucket — skipping (handled by the static-site path, not compute).`);
|
|
81936
82297
|
return false;
|
|
81937
82298
|
}
|
|
81938
82299
|
return true;
|
|
@@ -81984,6 +82345,7 @@ export {
|
|
|
81984
82345
|
validateTemplateSize,
|
|
81985
82346
|
validateTemplate,
|
|
81986
82347
|
validateResourceLimits,
|
|
82348
|
+
validateDeploymentConfig,
|
|
81987
82349
|
validateCredentials,
|
|
81988
82350
|
validateConfiguration,
|
|
81989
82351
|
validateCommand,
|
|
@@ -81999,6 +82361,7 @@ export {
|
|
|
81999
82361
|
suggestFlags,
|
|
82000
82362
|
suggestCommand,
|
|
82001
82363
|
storageAdvancedManager,
|
|
82364
|
+
staticSiteServerRoot,
|
|
82002
82365
|
staticSiteManager,
|
|
82003
82366
|
stackDependencyManager,
|
|
82004
82367
|
signRequestAsync,
|
|
@@ -82019,6 +82382,8 @@ export {
|
|
|
82019
82382
|
resolveStorageBucketName,
|
|
82020
82383
|
resolveSiteStackName,
|
|
82021
82384
|
resolveSiteResourceName,
|
|
82385
|
+
resolveSiteKind,
|
|
82386
|
+
resolveSiteDeployTarget,
|
|
82022
82387
|
resolveSiteBucketName,
|
|
82023
82388
|
resolveRegion,
|
|
82024
82389
|
resolveProjectStackName,
|
|
@@ -82028,11 +82393,13 @@ export {
|
|
|
82028
82393
|
resolveDeployBucketName,
|
|
82029
82394
|
resolveCredentials,
|
|
82030
82395
|
resolveCloudProvider,
|
|
82396
|
+
resolveCaddyfile,
|
|
82031
82397
|
requiresReplacement,
|
|
82032
82398
|
replicaManager,
|
|
82033
82399
|
regionPairManager,
|
|
82034
82400
|
quickHash,
|
|
82035
82401
|
queueManagementManager,
|
|
82402
|
+
proxyConfigFromSites,
|
|
82036
82403
|
providerEndpoint,
|
|
82037
82404
|
progressiveDeploymentManager,
|
|
82038
82405
|
processInChunks,
|
|
@@ -82065,6 +82432,7 @@ export {
|
|
|
82065
82432
|
lambdaConcurrencyManager,
|
|
82066
82433
|
isWebCryptoAvailable,
|
|
82067
82434
|
isValidRegion,
|
|
82435
|
+
isOnDemandDomain,
|
|
82068
82436
|
isNodeCryptoAvailable,
|
|
82069
82437
|
isLocalDevelopment,
|
|
82070
82438
|
isLikelyTypo,
|
|
@@ -82213,9 +82581,11 @@ export {
|
|
|
82213
82581
|
certificateManager,
|
|
82214
82582
|
categorizeChanges,
|
|
82215
82583
|
canaryManager,
|
|
82584
|
+
buildStaticSiteDeployScript,
|
|
82216
82585
|
buildSiteDeployScript,
|
|
82217
82586
|
buildOptimizationManager,
|
|
82218
82587
|
buildCloudFormationTemplate,
|
|
82588
|
+
buildCaddyfileFromProxy,
|
|
82219
82589
|
buildCaddyfile,
|
|
82220
82590
|
bounceComplaintHandler,
|
|
82221
82591
|
blueGreenManager,
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stacksjs/ts-cloud",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.2.
|
|
4
|
+
"version": "0.2.22",
|
|
5
5
|
"description": "A lightweight, performant infrastructure-as-code library and CLI for deploying both server-based (EC2) and serverless applications.",
|
|
6
6
|
"author": "Chris Breuer <chris@stacksjs.com>",
|
|
7
7
|
"license": "MIT",
|
|
@@ -89,8 +89,8 @@
|
|
|
89
89
|
"test": "bun test"
|
|
90
90
|
},
|
|
91
91
|
"dependencies": {
|
|
92
|
-
"@ts-cloud/aws-types": "0.2.
|
|
93
|
-
"@ts-cloud/core": "0.2.
|
|
92
|
+
"@ts-cloud/aws-types": "0.2.22",
|
|
93
|
+
"@ts-cloud/core": "0.2.22",
|
|
94
94
|
"@stacksjs/ts-xml": "^0.1.0"
|
|
95
95
|
},
|
|
96
96
|
"devDependencies": {
|