hackerrun 0.1.10 → 0.1.12
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/.claude/settings.local.json +2 -1
- package/dist/index.js +1669 -1607
- package/package.json +1 -1
- package/src/commands/scale.ts +158 -153
- package/src/commands/services.ts +84 -0
- package/src/index.ts +3 -1
- package/src/lib/cluster.ts +31 -62
- package/src/lib/platform-client.ts +27 -0
package/src/lib/cluster.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { AppCluster, VMNode, PlatformClient } from './platform-client.js';
|
|
2
2
|
import { SSHCertManager, testIPv6Connectivity } from './ssh-cert.js';
|
|
3
3
|
import { createTunnel, killTunnel, TunnelInfo } from './gateway-tunnel.js';
|
|
4
|
+
import { UncloudRunner } from './uncloud-runner.js';
|
|
4
5
|
import { execSync } from 'child_process';
|
|
5
6
|
import ora from 'ora';
|
|
6
7
|
import chalk from 'chalk';
|
|
@@ -18,11 +19,14 @@ export interface ClusterInitOptions {
|
|
|
18
19
|
|
|
19
20
|
export class ClusterManager {
|
|
20
21
|
private sshCertManager: SSHCertManager;
|
|
22
|
+
private uncloudRunner: UncloudRunner;
|
|
21
23
|
|
|
22
24
|
constructor(
|
|
23
|
-
private platformClient: PlatformClient
|
|
25
|
+
private platformClient: PlatformClient,
|
|
26
|
+
uncloudRunner?: UncloudRunner
|
|
24
27
|
) {
|
|
25
28
|
this.sshCertManager = new SSHCertManager(platformClient);
|
|
29
|
+
this.uncloudRunner = uncloudRunner || new UncloudRunner(platformClient);
|
|
26
30
|
}
|
|
27
31
|
|
|
28
32
|
/**
|
|
@@ -58,10 +62,7 @@ export class ClusterManager {
|
|
|
58
62
|
async initializeCluster(options: ClusterInitOptions): Promise<AppCluster> {
|
|
59
63
|
const { appName, location, vmSize, storageSize, bootImage } = options;
|
|
60
64
|
|
|
61
|
-
|
|
62
|
-
const vmName = `${appName}-1`;
|
|
63
|
-
|
|
64
|
-
let spinner = ora(`Creating VM '${vmName}' in ${location}...`).start();
|
|
65
|
+
let spinner = ora(`Creating VM for '${appName}' in ${location}...`).start();
|
|
65
66
|
|
|
66
67
|
try {
|
|
67
68
|
// Get platform SSH keys (for VM creation)
|
|
@@ -73,10 +74,9 @@ export class ClusterManager {
|
|
|
73
74
|
const privateSubnetId = gateway?.subnetId;
|
|
74
75
|
|
|
75
76
|
// Create IPv6-only VM via platform API with platform SSH key
|
|
76
|
-
// VM is
|
|
77
|
-
spinner.text =
|
|
78
|
-
const vm = await this.platformClient.
|
|
79
|
-
name: vmName,
|
|
77
|
+
// VM name is auto-generated by platform: u{userId}-{appName}-{nodeNum}
|
|
78
|
+
spinner.text = 'Creating VM...';
|
|
79
|
+
const vm = await this.platformClient.createAppVM(appName, {
|
|
80
80
|
location,
|
|
81
81
|
size: vmSize,
|
|
82
82
|
storage_size: storageSize,
|
|
@@ -85,9 +85,12 @@ export class ClusterManager {
|
|
|
85
85
|
public_key: platformKeys.platformPublicKey,
|
|
86
86
|
enable_ip4: !privateSubnetId, // IPv6-only if we have a subnet for NAT64
|
|
87
87
|
private_subnet_id: privateSubnetId,
|
|
88
|
+
isPrimary: true,
|
|
88
89
|
});
|
|
89
90
|
|
|
90
|
-
|
|
91
|
+
const vmName = vm.name; // Name generated by platform
|
|
92
|
+
|
|
93
|
+
spinner.text = `Waiting for VM '${vmName}' to be ready...`;
|
|
91
94
|
spinner.stop();
|
|
92
95
|
|
|
93
96
|
console.log(chalk.cyan('\nWaiting for VM to get an IPv6 address...'));
|
|
@@ -168,10 +171,7 @@ export class ClusterManager {
|
|
|
168
171
|
const gateway = await this.platformClient.getGateway(cluster.location);
|
|
169
172
|
const privateSubnetId = gateway?.subnetId;
|
|
170
173
|
|
|
171
|
-
|
|
172
|
-
const vmName = this.generateNodeName(appName, cluster.nodes);
|
|
173
|
-
|
|
174
|
-
let spinner = ora(`Adding node '${vmName}' to cluster...`).start();
|
|
174
|
+
let spinner = ora('Adding node to cluster...').start();
|
|
175
175
|
|
|
176
176
|
try {
|
|
177
177
|
// Get platform SSH keys (for VM creation)
|
|
@@ -179,9 +179,9 @@ export class ClusterManager {
|
|
|
179
179
|
const platformKeys = await this.getPlatformKeys();
|
|
180
180
|
|
|
181
181
|
// Create new IPv6-only VM via platform API with platform SSH key
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
182
|
+
// VM name is auto-generated by platform: u{userId}-{appName}-{nodeNum}
|
|
183
|
+
spinner.text = 'Creating VM...';
|
|
184
|
+
const vm = await this.platformClient.createAppVM(appName, {
|
|
185
185
|
location: cluster.location,
|
|
186
186
|
size: vmSize,
|
|
187
187
|
boot_image: bootImage,
|
|
@@ -189,9 +189,12 @@ export class ClusterManager {
|
|
|
189
189
|
public_key: platformKeys.platformPublicKey,
|
|
190
190
|
enable_ip4: !privateSubnetId,
|
|
191
191
|
private_subnet_id: privateSubnetId,
|
|
192
|
+
isPrimary: false,
|
|
192
193
|
});
|
|
193
194
|
|
|
194
|
-
|
|
195
|
+
const vmName = vm.name; // Name generated by platform
|
|
196
|
+
|
|
197
|
+
spinner.text = `Waiting for VM '${vmName}' to be ready...`;
|
|
195
198
|
spinner.stop();
|
|
196
199
|
console.log(chalk.cyan('\nWaiting for VM to get an IPv6 address...'));
|
|
197
200
|
const vmWithIp = await this.waitForVM(cluster.location, vmName, 600, false);
|
|
@@ -207,7 +210,7 @@ export class ClusterManager {
|
|
|
207
210
|
spinner.text = 'Joining uncloud cluster...';
|
|
208
211
|
spinner.stop();
|
|
209
212
|
console.log(chalk.cyan('\nJoining uncloud cluster...'));
|
|
210
|
-
await this.joinUncloudCluster(vmWithIp.ip6!,
|
|
213
|
+
await this.joinUncloudCluster(vmWithIp.ip6!, appName);
|
|
211
214
|
|
|
212
215
|
spinner = ora('Configuring Docker for NAT64...').start();
|
|
213
216
|
|
|
@@ -321,26 +324,17 @@ export class ClusterManager {
|
|
|
321
324
|
|
|
322
325
|
/**
|
|
323
326
|
* Join a new node to an existing uncloud cluster
|
|
324
|
-
* Uses
|
|
327
|
+
* Uses `uc machine add` which handles:
|
|
328
|
+
* - SSH to the new machine
|
|
329
|
+
* - Installing uncloudd if needed
|
|
330
|
+
* - Getting token from new machine via gRPC
|
|
331
|
+
* - Registering machine in cluster
|
|
325
332
|
*/
|
|
326
|
-
private async joinUncloudCluster(newVmIp: string,
|
|
333
|
+
private async joinUncloudCluster(newVmIp: string, appName: string): Promise<void> {
|
|
327
334
|
try {
|
|
328
|
-
//
|
|
329
|
-
|
|
330
|
-
await this.
|
|
331
|
-
|
|
332
|
-
// Get join token from primary node using --connect ssh://
|
|
333
|
-
const token = execSync(`uc --connect ssh://root@${primaryVmIp} machine token`, {
|
|
334
|
-
encoding: 'utf-8',
|
|
335
|
-
timeout: 30000,
|
|
336
|
-
}).trim();
|
|
337
|
-
|
|
338
|
-
if (!token) {
|
|
339
|
-
throw new Error('Failed to get join token from primary node');
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
// Join the new node to the cluster using --connect ssh://
|
|
343
|
-
execSync(`uc --connect ssh://root@${primaryVmIp} machine add root@${newVmIp} --token "${token}"`, {
|
|
335
|
+
// Use uc machine add which SSHes to the new VM and joins it to the cluster
|
|
336
|
+
// The --no-caddy flag skips Caddy deployment (we use the gateway for routing)
|
|
337
|
+
await this.uncloudRunner.run(appName, 'machine', ['add', `root@${newVmIp}`, '--no-caddy'], {
|
|
344
338
|
stdio: 'inherit',
|
|
345
339
|
timeout: 600000, // 10 min timeout
|
|
346
340
|
});
|
|
@@ -549,31 +543,6 @@ REMOTESCRIPT`, {
|
|
|
549
543
|
console.log(chalk.yellow(' Warning: Timeout waiting for unregistry, continuing anyway...'));
|
|
550
544
|
}
|
|
551
545
|
|
|
552
|
-
/**
|
|
553
|
-
* Generate a sequential node name: appName-1, appName-2, etc.
|
|
554
|
-
* Finds the next available number by checking existing node names
|
|
555
|
-
*/
|
|
556
|
-
private generateNodeName(appName: string, existingNodes: VMNode[]): string {
|
|
557
|
-
// Extract existing numbers from node names
|
|
558
|
-
const existingNumbers = existingNodes
|
|
559
|
-
.map(n => {
|
|
560
|
-
const match = n.name.match(new RegExp(`^${appName}-(\\d+)$`));
|
|
561
|
-
return match ? parseInt(match[1], 10) : 0;
|
|
562
|
-
})
|
|
563
|
-
.filter(n => n > 0);
|
|
564
|
-
|
|
565
|
-
// Find the next available number
|
|
566
|
-
const maxNumber = existingNumbers.length > 0 ? Math.max(...existingNumbers) : 0;
|
|
567
|
-
return `${appName}-${maxNumber + 1}`;
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
/**
|
|
571
|
-
* Generate a random ID (used for legacy naming or fallback)
|
|
572
|
-
*/
|
|
573
|
-
private generateId(): string {
|
|
574
|
-
return Math.random().toString(36).substring(2, 9);
|
|
575
|
-
}
|
|
576
|
-
|
|
577
546
|
/**
|
|
578
547
|
* Sleep for specified milliseconds
|
|
579
548
|
*/
|
|
@@ -64,6 +64,19 @@ export interface CreateVMParams {
|
|
|
64
64
|
private_subnet_id?: string; // Put VM in specific private subnet for NAT64 routing
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
// Parameters for creating an app VM (name is auto-generated by platform)
|
|
68
|
+
export interface CreateAppVMParams {
|
|
69
|
+
location: string;
|
|
70
|
+
size: string;
|
|
71
|
+
storage_size?: number;
|
|
72
|
+
unix_user: string;
|
|
73
|
+
public_key: string;
|
|
74
|
+
boot_image: string;
|
|
75
|
+
enable_ip4?: boolean;
|
|
76
|
+
private_subnet_id?: string;
|
|
77
|
+
isPrimary?: boolean;
|
|
78
|
+
}
|
|
79
|
+
|
|
67
80
|
export interface UbicloudVM {
|
|
68
81
|
id: string;
|
|
69
82
|
name: string;
|
|
@@ -274,6 +287,20 @@ export class PlatformClient {
|
|
|
274
287
|
return vm;
|
|
275
288
|
}
|
|
276
289
|
|
|
290
|
+
/**
|
|
291
|
+
* Create a new VM for an app with auto-generated name
|
|
292
|
+
* Name format: u{userId}-{appName}-{nodeNum}
|
|
293
|
+
*/
|
|
294
|
+
async createAppVM(appName: string, params: CreateAppVMParams): Promise<UbicloudVM> {
|
|
295
|
+
const { vm } = await this.request<{ vm: UbicloudVM }>(
|
|
296
|
+
'POST',
|
|
297
|
+
`/api/apps/${appName}/vms`,
|
|
298
|
+
params,
|
|
299
|
+
{ operation: `Create VM for app '${appName}'` }
|
|
300
|
+
);
|
|
301
|
+
return vm;
|
|
302
|
+
}
|
|
303
|
+
|
|
277
304
|
/**
|
|
278
305
|
* Get a specific VM
|
|
279
306
|
*/
|