@stacksjs/ts-cloud 0.2.18 → 0.2.20
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 +745 -745
- package/dist/drivers/hetzner/client.d.ts +12 -0
- package/dist/drivers/hetzner/driver.d.ts +9 -0
- package/dist/index.js +41 -5
- package/package.json +3 -3
|
@@ -86,6 +86,11 @@ export interface CreateFirewallOptions {
|
|
|
86
86
|
server: number;
|
|
87
87
|
}>;
|
|
88
88
|
}
|
|
89
|
+
export interface CreateSshKeyOptions {
|
|
90
|
+
name: string;
|
|
91
|
+
publicKey: string;
|
|
92
|
+
labels?: Record<string, string>;
|
|
93
|
+
}
|
|
89
94
|
export interface HetznerClientOptions {
|
|
90
95
|
apiToken: string;
|
|
91
96
|
baseUrl?: string;
|
|
@@ -115,6 +120,7 @@ export declare class HetznerClient {
|
|
|
115
120
|
server: number;
|
|
116
121
|
}>): Promise<HetznerAction[]>;
|
|
117
122
|
listSshKeys(): Promise<HetznerSshKey[]>;
|
|
123
|
+
createSshKey(options: CreateSshKeyOptions): Promise<HetznerSshKey>;
|
|
118
124
|
waitForAction(actionId: number, options?: {
|
|
119
125
|
pollIntervalMs?: number;
|
|
120
126
|
maxWaitMs?: number;
|
|
@@ -125,3 +131,9 @@ export declare class HetznerClient {
|
|
|
125
131
|
}): Promise<HetznerServer>;
|
|
126
132
|
}
|
|
127
133
|
export declare function resolveHetznerApiToken(configToken?: string): string;
|
|
134
|
+
/**
|
|
135
|
+
* Normalize an OpenSSH public key to its `<type> <base64>` body, dropping the
|
|
136
|
+
* trailing comment. Lets us match a local key against keys already registered
|
|
137
|
+
* in the Hetzner project regardless of differing comments/whitespace.
|
|
138
|
+
*/
|
|
139
|
+
export declare function normalizeSshPublicKey(publicKey: string): string;
|
|
@@ -3,6 +3,7 @@ import { HetznerClient } from './client';
|
|
|
3
3
|
export interface HetznerDriverOptions {
|
|
4
4
|
apiToken?: string;
|
|
5
5
|
sshPrivateKeyPath?: string;
|
|
6
|
+
sshPublicKeyPath?: string;
|
|
6
7
|
sshUser?: string;
|
|
7
8
|
location?: string;
|
|
8
9
|
client?: HetznerClient;
|
|
@@ -12,6 +13,7 @@ export declare class HetznerDriver implements CloudDriver {
|
|
|
12
13
|
readonly usesCloudFormation = false;
|
|
13
14
|
private client;
|
|
14
15
|
private sshPrivateKeyPath;
|
|
16
|
+
private sshPublicKeyPath;
|
|
15
17
|
private sshUser;
|
|
16
18
|
private location;
|
|
17
19
|
constructor(options?: HetznerDriverOptions);
|
|
@@ -20,6 +22,13 @@ export declare class HetznerDriver implements CloudDriver {
|
|
|
20
22
|
uploadRelease(options: UploadReleaseOptions): Promise<UploadReleaseResult>;
|
|
21
23
|
findComputeTargets(options: FindComputeTargetsOptions): Promise<ComputeTarget[]>;
|
|
22
24
|
runRemoteDeploy(options: RunRemoteDeployOptions): Promise<RemoteDeployResult>;
|
|
25
|
+
/**
|
|
26
|
+
* Ensure the local SSH public key is registered in the Hetzner project and
|
|
27
|
+
* return its id, so the freshly created server authorizes the same key the
|
|
28
|
+
* deploy step (SCP/SSH) uses. Without this, deploys fail with "Permission
|
|
29
|
+
* denied (publickey)" because the server has no authorized keys.
|
|
30
|
+
*/
|
|
31
|
+
private ensureSshKey;
|
|
23
32
|
private outputsFromState;
|
|
24
33
|
private sshBaseArgs;
|
|
25
34
|
private scpToHost;
|
package/dist/index.js
CHANGED
|
@@ -2041,12 +2041,12 @@ class S3Client2 {
|
|
|
2041
2041
|
headers[`x-amz-meta-${key}`] = value;
|
|
2042
2042
|
}
|
|
2043
2043
|
}
|
|
2044
|
+
const encodedKey = options.key.split("/").map((seg) => encodeURIComponent(seg)).join("/");
|
|
2044
2045
|
const normalizedBody = options.body instanceof Uint8Array && !Buffer.isBuffer(options.body) ? Buffer.from(options.body) : options.body;
|
|
2045
2046
|
if (Buffer.isBuffer(normalizedBody) || normalizedBody instanceof Uint8Array) {
|
|
2046
2047
|
const binaryBody = Buffer.isBuffer(normalizedBody) ? normalizedBody : Buffer.from(normalizedBody);
|
|
2047
2048
|
const { accessKeyId, secretAccessKey, sessionToken } = this.getCredentials();
|
|
2048
2049
|
const host = this.s3VirtualHost(options.bucket);
|
|
2049
|
-
const encodedKey = options.key.split("/").map((seg) => encodeURIComponent(seg)).join("/");
|
|
2050
2050
|
const url = `https://${host}/${encodedKey}`;
|
|
2051
2051
|
const now = new Date;
|
|
2052
2052
|
const amzDate = now.toISOString().replace(/[:-]|\.\d{3}/g, "");
|
|
@@ -2106,18 +2106,19 @@ class S3Client2 {
|
|
|
2106
2106
|
service: "s3",
|
|
2107
2107
|
region: this.region,
|
|
2108
2108
|
method: "PUT",
|
|
2109
|
-
path: `/${
|
|
2109
|
+
path: `/${encodedKey}`,
|
|
2110
2110
|
bucket: options.bucket,
|
|
2111
2111
|
headers,
|
|
2112
2112
|
body: options.body
|
|
2113
2113
|
});
|
|
2114
2114
|
}
|
|
2115
2115
|
async getObject(bucket, key) {
|
|
2116
|
+
const encodedKey = key.split("/").map((seg) => encodeURIComponent(seg)).join("/");
|
|
2116
2117
|
const result = await this.client.request({
|
|
2117
2118
|
service: "s3",
|
|
2118
2119
|
region: this.region,
|
|
2119
2120
|
method: "GET",
|
|
2120
|
-
path: `/${bucket}/${
|
|
2121
|
+
path: `/${bucket}/${encodedKey}`,
|
|
2121
2122
|
rawResponse: true
|
|
2122
2123
|
});
|
|
2123
2124
|
return result;
|
|
@@ -2146,11 +2147,12 @@ class S3Client2 {
|
|
|
2146
2147
|
});
|
|
2147
2148
|
}
|
|
2148
2149
|
async deleteObject(bucket, key) {
|
|
2150
|
+
const encodedKey = key.split("/").map((seg) => encodeURIComponent(seg)).join("/");
|
|
2149
2151
|
await this.client.request({
|
|
2150
2152
|
service: "s3",
|
|
2151
2153
|
region: this.region,
|
|
2152
2154
|
method: "DELETE",
|
|
2153
|
-
path: `/${bucket}/${
|
|
2155
|
+
path: `/${bucket}/${encodedKey}`
|
|
2154
2156
|
});
|
|
2155
2157
|
}
|
|
2156
2158
|
async deleteObjects(bucket, keys) {
|
|
@@ -81151,6 +81153,7 @@ class AwsDriver {
|
|
|
81151
81153
|
}
|
|
81152
81154
|
|
|
81153
81155
|
// src/drivers/hetzner/driver.ts
|
|
81156
|
+
import { existsSync as existsSync17, readFileSync as readFileSync10 } from "node:fs";
|
|
81154
81157
|
import { homedir as homedir7 } from "node:os";
|
|
81155
81158
|
import { join as join13 } from "node:path";
|
|
81156
81159
|
import { execSync } from "node:child_process";
|
|
@@ -81277,6 +81280,14 @@ class HetznerClient {
|
|
|
81277
81280
|
const data = await this.request("GET", "/ssh_keys");
|
|
81278
81281
|
return data.ssh_keys;
|
|
81279
81282
|
}
|
|
81283
|
+
async createSshKey(options) {
|
|
81284
|
+
const data = await this.request("POST", "/ssh_keys", {
|
|
81285
|
+
name: options.name,
|
|
81286
|
+
public_key: options.publicKey,
|
|
81287
|
+
labels: options.labels
|
|
81288
|
+
});
|
|
81289
|
+
return data.ssh_key;
|
|
81290
|
+
}
|
|
81280
81291
|
async waitForAction(actionId, options) {
|
|
81281
81292
|
const pollInterval = options?.pollIntervalMs ?? 2000;
|
|
81282
81293
|
const maxWait = options?.maxWaitMs ?? 300000;
|
|
@@ -81312,6 +81323,10 @@ function resolveHetznerApiToken(configToken) {
|
|
|
81312
81323
|
}
|
|
81313
81324
|
return token;
|
|
81314
81325
|
}
|
|
81326
|
+
function normalizeSshPublicKey(publicKey) {
|
|
81327
|
+
const [type, body] = publicKey.trim().split(/\s+/);
|
|
81328
|
+
return body ? `${type} ${body}` : type;
|
|
81329
|
+
}
|
|
81315
81330
|
|
|
81316
81331
|
// src/drivers/hetzner/cloud-init.ts
|
|
81317
81332
|
function generateUbuntuAppCloudInit(options = {}) {
|
|
@@ -81510,6 +81525,7 @@ class HetznerDriver {
|
|
|
81510
81525
|
usesCloudFormation = false;
|
|
81511
81526
|
client;
|
|
81512
81527
|
sshPrivateKeyPath;
|
|
81528
|
+
sshPublicKeyPath;
|
|
81513
81529
|
sshUser;
|
|
81514
81530
|
location;
|
|
81515
81531
|
constructor(options = {}) {
|
|
@@ -81517,6 +81533,7 @@ class HetznerDriver {
|
|
|
81517
81533
|
apiToken: resolveHetznerApiToken(options.apiToken)
|
|
81518
81534
|
});
|
|
81519
81535
|
this.sshPrivateKeyPath = expandHome(options.sshPrivateKeyPath || process.env.HCLOUD_SSH_KEY || "~/.ssh/id_ed25519");
|
|
81536
|
+
this.sshPublicKeyPath = expandHome(options.sshPublicKeyPath || process.env.HCLOUD_SSH_PUBLIC_KEY || `${this.sshPrivateKeyPath}.pub`);
|
|
81520
81537
|
this.sshUser = options.sshUser || process.env.HCLOUD_SSH_USER || "root";
|
|
81521
81538
|
this.location = options.location || process.env.HCLOUD_LOCATION || "fsn1";
|
|
81522
81539
|
}
|
|
@@ -81555,10 +81572,11 @@ class HetznerDriver {
|
|
|
81555
81572
|
name: firewallName,
|
|
81556
81573
|
labels,
|
|
81557
81574
|
rules: buildHetznerFirewallRules({
|
|
81558
|
-
allowSsh: compute.allowSsh,
|
|
81575
|
+
allowSsh: compute.allowSsh !== false,
|
|
81559
81576
|
sitePorts
|
|
81560
81577
|
})
|
|
81561
81578
|
});
|
|
81579
|
+
const sshKeyId = await this.ensureSshKey(slug, environment, labels);
|
|
81562
81580
|
const { server, action } = await this.client.createServer({
|
|
81563
81581
|
name: serverName,
|
|
81564
81582
|
serverType,
|
|
@@ -81566,6 +81584,7 @@ class HetznerDriver {
|
|
|
81566
81584
|
location: config6.hetzner?.location || this.location,
|
|
81567
81585
|
userData,
|
|
81568
81586
|
labels,
|
|
81587
|
+
sshKeys: sshKeyId ? [sshKeyId] : undefined,
|
|
81569
81588
|
firewalls: [{ firewall: firewall.id }]
|
|
81570
81589
|
});
|
|
81571
81590
|
await this.client.waitForAction(action.id);
|
|
@@ -81670,6 +81689,23 @@ class HetznerDriver {
|
|
|
81670
81689
|
error: success ? undefined : "One or more SSH deploy commands failed"
|
|
81671
81690
|
};
|
|
81672
81691
|
}
|
|
81692
|
+
async ensureSshKey(slug, environment, labels) {
|
|
81693
|
+
if (!existsSync17(this.sshPublicKeyPath)) {
|
|
81694
|
+
throw new Error(`SSH public key not found at ${this.sshPublicKeyPath}. ts-cloud deploys to Hetzner over SSH and needs a public key to authorize on the server. ` + `Generate one (\`ssh-keygen -t ed25519\`) or set hetzner.sshPrivateKeyPath / HCLOUD_SSH_PUBLIC_KEY.`);
|
|
81695
|
+
}
|
|
81696
|
+
const publicKey = readFileSync10(this.sshPublicKeyPath, "utf8").trim();
|
|
81697
|
+
const normalized = normalizeSshPublicKey(publicKey);
|
|
81698
|
+
const existing = await this.client.listSshKeys();
|
|
81699
|
+
const match = existing.find((key) => normalizeSshPublicKey(key.public_key) === normalized);
|
|
81700
|
+
if (match)
|
|
81701
|
+
return match.id;
|
|
81702
|
+
const created = await this.client.createSshKey({
|
|
81703
|
+
name: `${slug}-${environment}-deploy`,
|
|
81704
|
+
publicKey,
|
|
81705
|
+
labels
|
|
81706
|
+
});
|
|
81707
|
+
return created.id;
|
|
81708
|
+
}
|
|
81673
81709
|
outputsFromState(state, server) {
|
|
81674
81710
|
return {
|
|
81675
81711
|
deployStoragePath: state.deployStoragePath || "/var/ts-cloud/staging",
|
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.20",
|
|
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.20",
|
|
93
|
+
"@ts-cloud/core": "0.2.20",
|
|
94
94
|
"@stacksjs/ts-xml": "^0.1.0"
|
|
95
95
|
},
|
|
96
96
|
"devDependencies": {
|