@stacksjs/ts-cloud 0.6.2 → 0.7.2
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 +423 -412
- package/dist/drivers/hetzner/driver.d.ts +23 -0
- package/dist/drivers/hetzner/state.d.ts +8 -0
- package/dist/drivers/shared/rpx-gateway.d.ts +39 -3
- package/dist/index.js +250 -7
- 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/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
|
@@ -48,6 +48,29 @@ export declare class HetznerDriver implements CloudDriver {
|
|
|
48
48
|
* over the private network (wired into their `.env` at deploy time).
|
|
49
49
|
*/
|
|
50
50
|
private provisionFleet;
|
|
51
|
+
/**
|
|
52
|
+
* Provision a load-balanced **bun/node/deno** fleet: a private network, N
|
|
53
|
+
* app servers running the runtime directly (no local rpx gateway — the LB
|
|
54
|
+
* box reaches them over the private network), and ONE dedicated box running
|
|
55
|
+
* only the rpx gateway in load-balancing mode, fronting every `server-app`
|
|
56
|
+
* site's port across all app boxes (rpx v0.11.24+ multi-upstream routes with
|
|
57
|
+
* health-check failover — see {@link buildRpxLbConfig}).
|
|
58
|
+
*
|
|
59
|
+
* Mirrors {@link provisionFleet}'s idempotency/reconciliation/teardown
|
|
60
|
+
* patterns (reuse-by-label, create only the delta, destroy extras on
|
|
61
|
+
* scale-down), but fronts the app boxes with rpx instead of Hetzner's native
|
|
62
|
+
* Load Balancer product — this is the bun/rpx analogue of that PHP path.
|
|
63
|
+
*
|
|
64
|
+
* Edge case: a bun app that sets `compute.servicesServer` explicitly (wants
|
|
65
|
+
* a dedicated DB/cache box) but only ever runs one app server still lands
|
|
66
|
+
* here (see the `topology.dedicatedServices` dispatch condition) — in that
|
|
67
|
+
* case we still provision the dedicated services box (provider/runtime
|
|
68
|
+
* -agnostic infra, reusing the same PHP-fleet services-box mechanism is
|
|
69
|
+
* unnecessary since bun app servers need no PHP/nginx either way) but only
|
|
70
|
+
* stand up the rpx LB box when `topology.loadBalancer` is actually true
|
|
71
|
+
* (i.e. more than one app server, or `compute.server?.loadBalancer`).
|
|
72
|
+
*/
|
|
73
|
+
private provisionBunFleet;
|
|
51
74
|
/**
|
|
52
75
|
* Tear down the compute — single server or full fleet (load balancer, all
|
|
53
76
|
* app + services servers, firewalls, and the private network) — and clear
|
|
@@ -13,6 +13,14 @@ export interface HetznerDriverState {
|
|
|
13
13
|
loadBalancerId?: number;
|
|
14
14
|
servicesServerId?: number;
|
|
15
15
|
servicesPrivateIp?: string;
|
|
16
|
+
/**
|
|
17
|
+
* Bun+rpx fleet (see `HetznerDriver`'s bun-fleet path): the dedicated rpx
|
|
18
|
+
* load-balancer box (role `lb`) and the N app boxes (role `app`) it fronts.
|
|
19
|
+
* Distinct from {@link loadBalancerId} (a Hetzner-native Load Balancer
|
|
20
|
+
* resource, used by the PHP fleet path) — this is a real server running rpx.
|
|
21
|
+
*/
|
|
22
|
+
lbServerId?: number;
|
|
23
|
+
appServerIds?: number[];
|
|
16
24
|
}
|
|
17
25
|
export declare function driverStatePath(stackName: string): string;
|
|
18
26
|
export declare function readDriverState(stackName: string): Promise<HetznerDriverState | null>;
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* It replaces the old Caddyfile generation — pantry/stacks use rpx (their own
|
|
18
18
|
* tooling), so the gateway is rpx, not Caddy.
|
|
19
19
|
*/
|
|
20
|
-
import type { ComputeProxyConfig, SiteConfig, SiteRedirectConfig } from '@ts-cloud/core';
|
|
20
|
+
import type { ComputeProxyConfig, RpxLoadBalancerConfig, SiteConfig, SiteRedirectConfig } from '@ts-cloud/core';
|
|
21
21
|
/** Default directory on the box that holds real per-domain TLS certs. */
|
|
22
22
|
export declare const DEFAULT_RPX_CERTS_DIR = "/etc/rpx/certs";
|
|
23
23
|
/** Default webroot the gateway serves ACME http-01 challenges from on `:80`. */
|
|
@@ -40,8 +40,14 @@ export interface RpxRoute {
|
|
|
40
40
|
to: string;
|
|
41
41
|
/** Path prefix within the host this route owns (e.g. `/api`). Omitted = `/`. */
|
|
42
42
|
path?: string;
|
|
43
|
-
/**
|
|
44
|
-
|
|
43
|
+
/**
|
|
44
|
+
* Upstream(s) for a `server-app` route: a single `host:port` for a co-located
|
|
45
|
+
* (single-box) deploy, or an array of `host:port` — one per app box — when the
|
|
46
|
+
* route is fronted by a dedicated load-balancer box (see
|
|
47
|
+
* {@link buildRpxLbConfig}). rpx turns an array into a real load-balanced pool
|
|
48
|
+
* with automatic health-check failover (see rpx's `ProxyFrom`/`UpstreamTarget`).
|
|
49
|
+
*/
|
|
50
|
+
from?: string | string[];
|
|
45
51
|
/** Absolute directory served for a `server-static` route (`/var/www/<name>`). */
|
|
46
52
|
static?: string;
|
|
47
53
|
/**
|
|
@@ -65,6 +71,12 @@ export interface RpxRoute {
|
|
|
65
71
|
password: string;
|
|
66
72
|
realm?: string;
|
|
67
73
|
};
|
|
74
|
+
/**
|
|
75
|
+
* Load-balancing strategy/health-check tuning for a multi-upstream `from`
|
|
76
|
+
* (see {@link ComputeProxyConfig.loadBalancer}). Only meaningful when `from`
|
|
77
|
+
* is an array — rpx ignores it for a single-upstream route.
|
|
78
|
+
*/
|
|
79
|
+
loadBalancer?: RpxLoadBalancerConfig;
|
|
68
80
|
/** Stable id used when rpx registers the route. Derived from `to`+`path`. */
|
|
69
81
|
id: string;
|
|
70
82
|
}
|
|
@@ -143,8 +155,32 @@ export declare function deriveRouteId(to: string, path?: string): string;
|
|
|
143
155
|
* Routes are grouped by domain so rpx's path-based routing can serve an app +
|
|
144
156
|
* several static dirs under one host. Bucket sites and sites without a `domain`
|
|
145
157
|
* (or a `server-app` without a `port`) are skipped.
|
|
158
|
+
*
|
|
159
|
+
* This is the single-box path: every `server-app` route always resolves to
|
|
160
|
+
* `localhost:<port>` — unchanged, byte-for-byte, from before load-balanced
|
|
161
|
+
* fleets existed. Use {@link buildRpxLbConfig} for a dedicated LB box fronting
|
|
162
|
+
* more than one app box.
|
|
146
163
|
*/
|
|
147
164
|
export declare function buildRpxConfig(sites: Record<string, SiteConfig | undefined>, options: BuildRpxConfigOptions): RpxGatewayConfig;
|
|
165
|
+
/** An app box's addresses, as known to the LB box building routes to it. */
|
|
166
|
+
export interface RpxLbAppBox {
|
|
167
|
+
/** Private IP of the app box, reachable from the LB over the fleet's private network. Preferred. */
|
|
168
|
+
privateIp?: string;
|
|
169
|
+
/** Public IP of the app box — used only when no private IP is available. */
|
|
170
|
+
publicIp?: string;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Build the rpx gateway config for a **dedicated load-balancer box**: like
|
|
174
|
+
* {@link buildRpxConfig}, but every `server-app` route's `from` is an array of
|
|
175
|
+
* `host:port` — one per entry in `appBoxes` (private IP preferred, public IP as
|
|
176
|
+
* fallback) — instead of `localhost:<port>`. rpx turns that array into a real
|
|
177
|
+
* load-balanced pool with health-check failover (see rpx's `ProxyFrom`).
|
|
178
|
+
*
|
|
179
|
+
* `server-static`/`redirect` routes are unaffected (the LB box doesn't serve
|
|
180
|
+
* static files or own redirects itself in the primary bun-fleet flow — those
|
|
181
|
+
* kinds simply pass through unchanged if present in `sites`).
|
|
182
|
+
*/
|
|
183
|
+
export declare function buildRpxLbConfig(sites: Record<string, SiteConfig | undefined>, appBoxes: RpxLbAppBox[], options: BuildRpxConfigOptions): RpxGatewayConfig;
|
|
148
184
|
/**
|
|
149
185
|
* Render the rpx gateway config as a self-contained launcher TS module. The
|
|
150
186
|
* systemd unit runs `bun <file>`, which imports `startProxies` from the
|
package/dist/index.js
CHANGED
|
@@ -85584,6 +85584,17 @@ ${servicesProvision.join(`
|
|
|
85584
85584
|
# breaking a from-scratch provision (adopting an existing box masked it).
|
|
85585
85585
|
export HOME="\${HOME:-/root}"
|
|
85586
85586
|
export BUN_INSTALL="/root/.bun"
|
|
85587
|
+
# bun.sh's installer references $HOME internally and runs under this
|
|
85588
|
+
# script's \`set -u\` (inherited by the piped bash) — cloud-init's runcmd
|
|
85589
|
+
# environment doesn't export HOME by default, so without this the
|
|
85590
|
+
# installer dies with "HOME: unbound variable" partway through (after
|
|
85591
|
+
# the binary is already downloaded, so it looks like a partial success)
|
|
85592
|
+
# and \`set -e\` aborts everything after it: the bun symlink, /var/www +
|
|
85593
|
+
# /var/ts-cloud dirs, and the whole rpx gateway install/systemd setup
|
|
85594
|
+
# never run. Confirmed against a real Hetzner box (stacksjs/status#1
|
|
85595
|
+
# Phase 9 e2e deploy) — cloud-init reported success with none of that
|
|
85596
|
+
# actually done.
|
|
85597
|
+
export HOME="\${HOME:-/root}"
|
|
85587
85598
|
curl -fsSL https://bun.sh/install | bash${runtimeVersion === "latest" ? "" : ` -s "bun-v${runtimeVersion}"`}
|
|
85588
85599
|
ln -sf /root/.bun/bin/bun /usr/local/bin/bun
|
|
85589
85600
|
echo 'export BUN_INSTALL="/root/.bun"' > /etc/profile.d/bun.sh
|
|
@@ -86230,9 +86241,15 @@ function deriveRouteId(to, path) {
|
|
|
86230
86241
|
const cleaned = base.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 128);
|
|
86231
86242
|
return cleaned.length > 0 ? cleaned : "rpx";
|
|
86232
86243
|
}
|
|
86233
|
-
function
|
|
86244
|
+
function resolveServerAppFrom(port, appBoxes) {
|
|
86245
|
+
if (!appBoxes || appBoxes.length === 0)
|
|
86246
|
+
return `localhost:${port}`;
|
|
86247
|
+
return appBoxes.map((box2) => `${box2.privateIp ?? box2.publicIp}:${port}`);
|
|
86248
|
+
}
|
|
86249
|
+
function buildRpxConfigInternal(sites, options, appBoxes) {
|
|
86234
86250
|
const wwwRoot = (options.wwwRoot ?? "/var/www").replace(/\/+$/, "");
|
|
86235
86251
|
const certsDir = options.proxy.certsDir ?? DEFAULT_RPX_CERTS_DIR;
|
|
86252
|
+
const loadBalancer = options.proxy.loadBalancer;
|
|
86236
86253
|
const proxies = [];
|
|
86237
86254
|
const domains = new Set;
|
|
86238
86255
|
for (const [name, site] of Object.entries(sites)) {
|
|
@@ -86252,7 +86269,15 @@ function buildRpxConfig(sites, options) {
|
|
|
86252
86269
|
if (kind === "server-app") {
|
|
86253
86270
|
if (typeof site.port !== "number")
|
|
86254
86271
|
continue;
|
|
86255
|
-
|
|
86272
|
+
const from = resolveServerAppFrom(site.port, appBoxes);
|
|
86273
|
+
proxies.push({
|
|
86274
|
+
to: site.domain,
|
|
86275
|
+
path,
|
|
86276
|
+
from,
|
|
86277
|
+
id,
|
|
86278
|
+
...auth ? { auth } : {},
|
|
86279
|
+
...Array.isArray(from) && loadBalancer ? { loadBalancer } : {}
|
|
86280
|
+
});
|
|
86256
86281
|
} else {
|
|
86257
86282
|
proxies.push({
|
|
86258
86283
|
to: site.domain,
|
|
@@ -86299,6 +86324,12 @@ function buildRpxConfig(sites, options) {
|
|
|
86299
86324
|
}
|
|
86300
86325
|
return config6;
|
|
86301
86326
|
}
|
|
86327
|
+
function buildRpxConfig(sites, options) {
|
|
86328
|
+
return buildRpxConfigInternal(sites, options);
|
|
86329
|
+
}
|
|
86330
|
+
function buildRpxLbConfig(sites, appBoxes, options) {
|
|
86331
|
+
return buildRpxConfigInternal(sites, options, appBoxes);
|
|
86332
|
+
}
|
|
86302
86333
|
var RPX_DIR = "/etc/rpx";
|
|
86303
86334
|
var RPX_INSTALL_DIR = "/opt/rpx-gateway";
|
|
86304
86335
|
var RPX_LAUNCHER_PATH = "/etc/rpx/gateway.ts";
|
|
@@ -86637,9 +86668,11 @@ class HetznerDriver {
|
|
|
86637
86668
|
}
|
|
86638
86669
|
const stackName = resolveProjectStackName(config6, environment);
|
|
86639
86670
|
const serverName = `${slug}-${environment}-app`;
|
|
86671
|
+
const phpBox = compute.runtime === "php" || !!compute.php;
|
|
86640
86672
|
const topology = resolveFleetTopology(compute);
|
|
86641
|
-
if (topology.dedicatedServices || topology.appServers > 1)
|
|
86642
|
-
return this.provisionFleet(options, topology);
|
|
86673
|
+
if (topology.dedicatedServices || topology.appServers > 1) {
|
|
86674
|
+
return phpBox ? this.provisionFleet(options, topology) : this.provisionBunFleet(options, topology);
|
|
86675
|
+
}
|
|
86643
86676
|
const existing = await readDriverState(stackName);
|
|
86644
86677
|
if (existing?.serverId) {
|
|
86645
86678
|
const server2 = await this.tryGetServer(existing.serverId);
|
|
@@ -86876,6 +86909,202 @@ class HetznerDriver {
|
|
|
86876
86909
|
sshUser: this.sshUser
|
|
86877
86910
|
};
|
|
86878
86911
|
}
|
|
86912
|
+
async provisionBunFleet(options, topology) {
|
|
86913
|
+
const { config: config6, environment } = options;
|
|
86914
|
+
const slug = config6.project.slug;
|
|
86915
|
+
const compute = config6.infrastructure.compute;
|
|
86916
|
+
const stackName = resolveProjectStackName(config6, environment);
|
|
86917
|
+
const serverType = resolveHetznerServerType(compute.size);
|
|
86918
|
+
const image = compute.image || config6.hetzner?.image || "ubuntu-24.04";
|
|
86919
|
+
const location = config6.hetzner?.location || this.location;
|
|
86920
|
+
const baked = compute.bakedImage === true;
|
|
86921
|
+
const sites = config6.sites || {};
|
|
86922
|
+
const existingState = await readDriverState(stackName);
|
|
86923
|
+
if (existingState?.lbServerId) {
|
|
86924
|
+
const lb = await this.tryGetServer(existingState.lbServerId);
|
|
86925
|
+
if (lb && lb.status !== "off") {
|
|
86926
|
+
return {
|
|
86927
|
+
appPublicIp: lb.public_net.ipv4?.ip ?? existingState.publicIp,
|
|
86928
|
+
loadBalancerIp: lb.public_net.ipv4?.ip ?? existingState.publicIp,
|
|
86929
|
+
servicesPrivateIp: existingState.servicesPrivateIp,
|
|
86930
|
+
deployStoragePath: "/var/ts-cloud/staging",
|
|
86931
|
+
sshUser: this.sshUser
|
|
86932
|
+
};
|
|
86933
|
+
}
|
|
86934
|
+
}
|
|
86935
|
+
const sshKeyId = await this.ensureSshKey(slug, environment, tsCloudLabels(slug, environment, "app"));
|
|
86936
|
+
const netName = `${slug}-${environment}-net`;
|
|
86937
|
+
const networks = await this.client.listNetworks().catch(() => []);
|
|
86938
|
+
const network = networks.find((n) => n.name === netName) ?? await this.client.createNetwork({ name: netName, labels: tsCloudLabels(slug, environment, "app") });
|
|
86939
|
+
const sitePorts = this.collectUpstreamPorts(sites);
|
|
86940
|
+
const { firewall: appFw } = await this.ensureFirewall(`${slug}-${environment}-app-fw`, tsCloudLabels(slug, environment, "app"), [
|
|
86941
|
+
{ direction: "in", protocol: "tcp", port: "22", source_ips: ["0.0.0.0/0", "::/0"] },
|
|
86942
|
+
...sitePorts.map((port) => ({
|
|
86943
|
+
direction: "in",
|
|
86944
|
+
protocol: "tcp",
|
|
86945
|
+
port: String(port),
|
|
86946
|
+
source_ips: [network.ip_range],
|
|
86947
|
+
description: `ts-cloud port ${port} (private, LB-only)`
|
|
86948
|
+
}))
|
|
86949
|
+
]);
|
|
86950
|
+
const { firewall: lbFw } = await this.ensureFirewall(`${slug}-${environment}-lb-fw`, tsCloudLabels(slug, environment, "lb"), buildHetznerFirewallRules({ allowSsh: true, sitePorts: [] }));
|
|
86951
|
+
const all = await this.client.listServers().catch(() => []);
|
|
86952
|
+
const newServerIds = [];
|
|
86953
|
+
let servicesServerId;
|
|
86954
|
+
let servicesPrivateIp;
|
|
86955
|
+
if (topology.dedicatedServices) {
|
|
86956
|
+
const { firewall: svcFw } = await this.ensureFirewall(`${slug}-${environment}-services-fw`, tsCloudLabels(slug, environment, "services"), [
|
|
86957
|
+
{ direction: "in", protocol: "tcp", port: "22", source_ips: ["0.0.0.0/0", "::/0"] },
|
|
86958
|
+
{ direction: "in", protocol: "tcp", port: "3306", source_ips: [network.ip_range] },
|
|
86959
|
+
{ direction: "in", protocol: "tcp", port: "5432", source_ips: [network.ip_range] },
|
|
86960
|
+
{ direction: "in", protocol: "tcp", port: "6379", source_ips: [network.ip_range] },
|
|
86961
|
+
{ direction: "in", protocol: "tcp", port: "7700", source_ips: [network.ip_range] }
|
|
86962
|
+
]);
|
|
86963
|
+
const servicesProvision = [
|
|
86964
|
+
...buildServicesProvisionScript(compute.managedServices ?? { mysql: true, redis: true }, { bindPrivate: true }),
|
|
86965
|
+
...buildDatabaseSetupScript(config6.infrastructure?.appDatabase, compute.managedServices ?? { mysql: true }),
|
|
86966
|
+
...buildAutoUpdatesScript(true),
|
|
86967
|
+
...buildMonitoringScript(true),
|
|
86968
|
+
...buildAuthorizedKeysScript(compute.sshKeys)
|
|
86969
|
+
];
|
|
86970
|
+
const servicesUserData = wrapCloudInitUserData(buildUbuntuBootstrapScript({ runtime: "bun", servicesProvision, baked }));
|
|
86971
|
+
let svcServer = all.find((s) => matchesTsCloudLabels(s.labels, slug, environment, "services"));
|
|
86972
|
+
if (!svcServer) {
|
|
86973
|
+
const { server, action } = await this.client.createServer({
|
|
86974
|
+
name: `${slug}-${environment}-services`,
|
|
86975
|
+
serverType: resolveHetznerServerType(typeof compute.servicesServer === "object" ? compute.servicesServer.size : compute.size),
|
|
86976
|
+
image,
|
|
86977
|
+
location,
|
|
86978
|
+
userData: servicesUserData,
|
|
86979
|
+
labels: tsCloudLabels(slug, environment, "services"),
|
|
86980
|
+
sshKeys: sshKeyId ? [sshKeyId] : undefined,
|
|
86981
|
+
firewalls: [{ firewall: svcFw.id }],
|
|
86982
|
+
networks: [network.id]
|
|
86983
|
+
});
|
|
86984
|
+
await this.client.waitForAction(action.id);
|
|
86985
|
+
svcServer = await this.client.waitForServerRunning(server.id);
|
|
86986
|
+
newServerIds.push(server.id);
|
|
86987
|
+
}
|
|
86988
|
+
servicesServerId = svcServer.id;
|
|
86989
|
+
servicesPrivateIp = svcServer.private_net?.[0]?.ip ?? (await this.client.getServer(servicesServerId)).private_net?.[0]?.ip;
|
|
86990
|
+
}
|
|
86991
|
+
const appProvisionScripts = buildComputeProvisionScripts(config6);
|
|
86992
|
+
const appUserData = wrapCloudInitUserData(buildUbuntuBootstrapScript({
|
|
86993
|
+
runtime: appProvisionScripts.runtime,
|
|
86994
|
+
runtimeVersion: appProvisionScripts.runtimeVersion,
|
|
86995
|
+
systemPackages: compute.systemPackages,
|
|
86996
|
+
database: config6.infrastructure?.database,
|
|
86997
|
+
servicesProvision: appProvisionScripts.servicesProvision,
|
|
86998
|
+
baked
|
|
86999
|
+
}));
|
|
87000
|
+
const existingApp = all.filter((s) => matchesTsCloudLabels(s.labels, slug, environment, "app"));
|
|
87001
|
+
const appServerIds = existingApp.map((s) => s.id);
|
|
87002
|
+
if (existingApp.length > topology.appServers) {
|
|
87003
|
+
for (const extra of existingApp.slice(topology.appServers)) {
|
|
87004
|
+
await this.client.deleteServer(extra.id).catch(() => {});
|
|
87005
|
+
appServerIds.splice(appServerIds.indexOf(extra.id), 1);
|
|
87006
|
+
}
|
|
87007
|
+
}
|
|
87008
|
+
for (let i = existingApp.length;i < topology.appServers; i++) {
|
|
87009
|
+
const { server, action } = await this.client.createServer({
|
|
87010
|
+
name: `${slug}-${environment}-app-${i + 1}`,
|
|
87011
|
+
serverType,
|
|
87012
|
+
image,
|
|
87013
|
+
location,
|
|
87014
|
+
userData: appUserData,
|
|
87015
|
+
labels: tsCloudLabels(slug, environment, "app"),
|
|
87016
|
+
sshKeys: sshKeyId ? [sshKeyId] : undefined,
|
|
87017
|
+
firewalls: [{ firewall: appFw.id }],
|
|
87018
|
+
networks: [network.id]
|
|
87019
|
+
});
|
|
87020
|
+
await this.client.waitForAction(action.id);
|
|
87021
|
+
appServerIds.push(server.id);
|
|
87022
|
+
newServerIds.push(server.id);
|
|
87023
|
+
}
|
|
87024
|
+
if (this.waitForBoot && newServerIds.length > 0) {
|
|
87025
|
+
await Promise.all(newServerIds.map(async (id) => {
|
|
87026
|
+
const running = await this.client.waitForServerRunning(id);
|
|
87027
|
+
const ip = running.public_net.ipv4?.ip;
|
|
87028
|
+
if (ip) {
|
|
87029
|
+
await this.waitForSshReady(ip);
|
|
87030
|
+
await this.waitForCloudInit(ip);
|
|
87031
|
+
}
|
|
87032
|
+
}));
|
|
87033
|
+
}
|
|
87034
|
+
const appBoxes = [];
|
|
87035
|
+
for (const id of appServerIds) {
|
|
87036
|
+
let server = all.find((s) => s.id === id);
|
|
87037
|
+
if (!server || !server.private_net?.[0]?.ip)
|
|
87038
|
+
server = await this.client.getServer(id).catch(() => server);
|
|
87039
|
+
appBoxes.push({ privateIp: server?.private_net?.[0]?.ip, publicIp: server?.public_net.ipv4?.ip });
|
|
87040
|
+
}
|
|
87041
|
+
const lbName = `${slug}-${environment}-lb`;
|
|
87042
|
+
let lbId;
|
|
87043
|
+
let lbIp;
|
|
87044
|
+
if (topology.loadBalancer) {
|
|
87045
|
+
let lbServer = all.find((s) => matchesTsCloudLabels(s.labels, slug, environment, "lb"));
|
|
87046
|
+
if (!lbServer) {
|
|
87047
|
+
const rpxProxy = compute.proxy?.engine === "rpx" ? compute.proxy : { engine: "rpx" };
|
|
87048
|
+
const lbRpxProvision = buildRpxProvisionScript({
|
|
87049
|
+
proxy: rpxProxy,
|
|
87050
|
+
config: buildRpxLbConfig(sites, appBoxes, { proxy: rpxProxy }),
|
|
87051
|
+
slug,
|
|
87052
|
+
bunBin: appProvisionScripts.runtime === "node" || appProvisionScripts.runtime === "deno" ? undefined : "/usr/local/bin/bun"
|
|
87053
|
+
});
|
|
87054
|
+
const lbUserData = wrapCloudInitUserData(buildUbuntuBootstrapScript({
|
|
87055
|
+
runtime: "bun",
|
|
87056
|
+
rpxProvision: lbRpxProvision,
|
|
87057
|
+
baked: false
|
|
87058
|
+
}));
|
|
87059
|
+
const lbSize = typeof compute.loadBalancer === "object" ? compute.loadBalancer.size : undefined;
|
|
87060
|
+
const { server, action } = await this.client.createServer({
|
|
87061
|
+
name: lbName,
|
|
87062
|
+
serverType: resolveHetznerServerType(lbSize ?? "micro"),
|
|
87063
|
+
image,
|
|
87064
|
+
location,
|
|
87065
|
+
userData: lbUserData,
|
|
87066
|
+
labels: tsCloudLabels(slug, environment, "lb"),
|
|
87067
|
+
sshKeys: sshKeyId ? [sshKeyId] : undefined,
|
|
87068
|
+
firewalls: [{ firewall: lbFw.id }],
|
|
87069
|
+
networks: [network.id]
|
|
87070
|
+
});
|
|
87071
|
+
await this.client.waitForAction(action.id);
|
|
87072
|
+
lbServer = await this.client.waitForServerRunning(server.id);
|
|
87073
|
+
if (this.waitForBoot) {
|
|
87074
|
+
const ip = lbServer.public_net.ipv4?.ip;
|
|
87075
|
+
if (ip) {
|
|
87076
|
+
await this.waitForSshReady(ip);
|
|
87077
|
+
await this.waitForCloudInit(ip);
|
|
87078
|
+
}
|
|
87079
|
+
}
|
|
87080
|
+
}
|
|
87081
|
+
lbId = lbServer.id;
|
|
87082
|
+
lbIp = lbServer.public_net.ipv4?.ip;
|
|
87083
|
+
}
|
|
87084
|
+
const appPublicIp = lbIp ?? (await this.client.getServer(appServerIds[0]).catch(() => {
|
|
87085
|
+
return;
|
|
87086
|
+
}))?.public_net.ipv4?.ip;
|
|
87087
|
+
const state = {
|
|
87088
|
+
provider: "hetzner",
|
|
87089
|
+
stackName,
|
|
87090
|
+
networkId: network.id,
|
|
87091
|
+
lbServerId: lbId,
|
|
87092
|
+
appServerIds,
|
|
87093
|
+
servicesServerId,
|
|
87094
|
+
servicesPrivateIp,
|
|
87095
|
+
publicIp: appPublicIp,
|
|
87096
|
+
deployStoragePath: "/var/ts-cloud/staging",
|
|
87097
|
+
sshUser: this.sshUser
|
|
87098
|
+
};
|
|
87099
|
+
await writeDriverState(stackName, state);
|
|
87100
|
+
return {
|
|
87101
|
+
appPublicIp,
|
|
87102
|
+
loadBalancerIp: lbIp,
|
|
87103
|
+
servicesPrivateIp,
|
|
87104
|
+
deployStoragePath: "/var/ts-cloud/staging",
|
|
87105
|
+
sshUser: this.sshUser
|
|
87106
|
+
};
|
|
87107
|
+
}
|
|
86879
87108
|
async destroyCompute(options) {
|
|
86880
87109
|
const { config: config6, environment } = options;
|
|
86881
87110
|
const slug = config6.project.slug;
|
|
@@ -86894,13 +87123,17 @@ class HetznerDriver {
|
|
|
86894
87123
|
const allServers = await this.client.listServers().catch(() => []);
|
|
86895
87124
|
const serverIds = new Set;
|
|
86896
87125
|
for (const s of allServers) {
|
|
86897
|
-
if (matchesTsCloudLabels(s.labels, slug, environment, "app") || matchesTsCloudLabels(s.labels, slug, environment, "services"))
|
|
87126
|
+
if (matchesTsCloudLabels(s.labels, slug, environment, "app") || matchesTsCloudLabels(s.labels, slug, environment, "services") || matchesTsCloudLabels(s.labels, slug, environment, "lb"))
|
|
86898
87127
|
serverIds.add(s.id);
|
|
86899
87128
|
}
|
|
86900
87129
|
if (state?.serverId)
|
|
86901
87130
|
serverIds.add(state.serverId);
|
|
86902
87131
|
if (state?.servicesServerId)
|
|
86903
87132
|
serverIds.add(state.servicesServerId);
|
|
87133
|
+
if (state?.lbServerId)
|
|
87134
|
+
serverIds.add(state.lbServerId);
|
|
87135
|
+
for (const id of state?.appServerIds ?? [])
|
|
87136
|
+
serverIds.add(id);
|
|
86904
87137
|
for (const id of serverIds) {
|
|
86905
87138
|
try {
|
|
86906
87139
|
await this.client.deleteServer(id);
|
|
@@ -86908,7 +87141,7 @@ class HetznerDriver {
|
|
|
86908
87141
|
} catch {}
|
|
86909
87142
|
}
|
|
86910
87143
|
const firewalls = await this.client.listFirewalls().catch(() => []);
|
|
86911
|
-
for (const name of [`${slug}-${environment}-app-fw`, `${slug}-${environment}-services-fw`]) {
|
|
87144
|
+
for (const name of [`${slug}-${environment}-app-fw`, `${slug}-${environment}-services-fw`, `${slug}-${environment}-lb-fw`]) {
|
|
86912
87145
|
const fw = firewalls.find((f) => f.name === name);
|
|
86913
87146
|
if (!fw)
|
|
86914
87147
|
continue;
|
|
@@ -86946,6 +87179,15 @@ class HetznerDriver {
|
|
|
86946
87179
|
const server = await this.client.getServer(state.serverId);
|
|
86947
87180
|
return this.outputsFromState(state, server);
|
|
86948
87181
|
}
|
|
87182
|
+
if (state?.lbServerId) {
|
|
87183
|
+
const lb = await this.tryGetServer(state.lbServerId);
|
|
87184
|
+
const lbIp = lb?.public_net.ipv4?.ip ?? state.publicIp;
|
|
87185
|
+
return {
|
|
87186
|
+
...this.outputsFromState(state),
|
|
87187
|
+
appPublicIp: lbIp,
|
|
87188
|
+
loadBalancerIp: lbIp
|
|
87189
|
+
};
|
|
87190
|
+
}
|
|
86949
87191
|
if (state?.loadBalancerId || state?.servicesPrivateIp) {
|
|
86950
87192
|
return this.outputsFromState(state);
|
|
86951
87193
|
}
|
|
@@ -86971,7 +87213,8 @@ class HetznerDriver {
|
|
|
86971
87213
|
if (targets.length === 0) {
|
|
86972
87214
|
throw new Error("No Hetzner compute targets found for release upload");
|
|
86973
87215
|
}
|
|
86974
|
-
const
|
|
87216
|
+
const stagingName = options.remoteKey.replace(/^releases\//, "").replace(/\//g, "-");
|
|
87217
|
+
const remotePath = `/var/ts-cloud/staging/${stagingName}`;
|
|
86975
87218
|
for (const target of targets) {
|
|
86976
87219
|
if (!target.publicIp) {
|
|
86977
87220
|
throw new Error(`Target ${target.id} has no public IP for SCP upload`);
|