@stacksjs/ts-cloud 0.2.19 → 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 +35 -1
- 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
|
@@ -81153,6 +81153,7 @@ class AwsDriver {
|
|
|
81153
81153
|
}
|
|
81154
81154
|
|
|
81155
81155
|
// src/drivers/hetzner/driver.ts
|
|
81156
|
+
import { existsSync as existsSync17, readFileSync as readFileSync10 } from "node:fs";
|
|
81156
81157
|
import { homedir as homedir7 } from "node:os";
|
|
81157
81158
|
import { join as join13 } from "node:path";
|
|
81158
81159
|
import { execSync } from "node:child_process";
|
|
@@ -81279,6 +81280,14 @@ class HetznerClient {
|
|
|
81279
81280
|
const data = await this.request("GET", "/ssh_keys");
|
|
81280
81281
|
return data.ssh_keys;
|
|
81281
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
|
+
}
|
|
81282
81291
|
async waitForAction(actionId, options) {
|
|
81283
81292
|
const pollInterval = options?.pollIntervalMs ?? 2000;
|
|
81284
81293
|
const maxWait = options?.maxWaitMs ?? 300000;
|
|
@@ -81314,6 +81323,10 @@ function resolveHetznerApiToken(configToken) {
|
|
|
81314
81323
|
}
|
|
81315
81324
|
return token;
|
|
81316
81325
|
}
|
|
81326
|
+
function normalizeSshPublicKey(publicKey) {
|
|
81327
|
+
const [type, body] = publicKey.trim().split(/\s+/);
|
|
81328
|
+
return body ? `${type} ${body}` : type;
|
|
81329
|
+
}
|
|
81317
81330
|
|
|
81318
81331
|
// src/drivers/hetzner/cloud-init.ts
|
|
81319
81332
|
function generateUbuntuAppCloudInit(options = {}) {
|
|
@@ -81512,6 +81525,7 @@ class HetznerDriver {
|
|
|
81512
81525
|
usesCloudFormation = false;
|
|
81513
81526
|
client;
|
|
81514
81527
|
sshPrivateKeyPath;
|
|
81528
|
+
sshPublicKeyPath;
|
|
81515
81529
|
sshUser;
|
|
81516
81530
|
location;
|
|
81517
81531
|
constructor(options = {}) {
|
|
@@ -81519,6 +81533,7 @@ class HetznerDriver {
|
|
|
81519
81533
|
apiToken: resolveHetznerApiToken(options.apiToken)
|
|
81520
81534
|
});
|
|
81521
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`);
|
|
81522
81537
|
this.sshUser = options.sshUser || process.env.HCLOUD_SSH_USER || "root";
|
|
81523
81538
|
this.location = options.location || process.env.HCLOUD_LOCATION || "fsn1";
|
|
81524
81539
|
}
|
|
@@ -81557,10 +81572,11 @@ class HetznerDriver {
|
|
|
81557
81572
|
name: firewallName,
|
|
81558
81573
|
labels,
|
|
81559
81574
|
rules: buildHetznerFirewallRules({
|
|
81560
|
-
allowSsh: compute.allowSsh,
|
|
81575
|
+
allowSsh: compute.allowSsh !== false,
|
|
81561
81576
|
sitePorts
|
|
81562
81577
|
})
|
|
81563
81578
|
});
|
|
81579
|
+
const sshKeyId = await this.ensureSshKey(slug, environment, labels);
|
|
81564
81580
|
const { server, action } = await this.client.createServer({
|
|
81565
81581
|
name: serverName,
|
|
81566
81582
|
serverType,
|
|
@@ -81568,6 +81584,7 @@ class HetznerDriver {
|
|
|
81568
81584
|
location: config6.hetzner?.location || this.location,
|
|
81569
81585
|
userData,
|
|
81570
81586
|
labels,
|
|
81587
|
+
sshKeys: sshKeyId ? [sshKeyId] : undefined,
|
|
81571
81588
|
firewalls: [{ firewall: firewall.id }]
|
|
81572
81589
|
});
|
|
81573
81590
|
await this.client.waitForAction(action.id);
|
|
@@ -81672,6 +81689,23 @@ class HetznerDriver {
|
|
|
81672
81689
|
error: success ? undefined : "One or more SSH deploy commands failed"
|
|
81673
81690
|
};
|
|
81674
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
|
+
}
|
|
81675
81709
|
outputsFromState(state, server) {
|
|
81676
81710
|
return {
|
|
81677
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": {
|