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.
@@ -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
- // First node is always appName-1
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 placed in the same private subnet as the gateway for NAT64 routing
77
- spinner.text = `Creating VM '${vmName}'...`;
78
- const vm = await this.platformClient.createVM({
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
- spinner.text = `Waiting for VM to be ready...`;
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
- // Generate sequential node name: appName-1, appName-2, etc.
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
- spinner.text = `Creating VM '${vmName}'...`;
183
- const vm = await this.platformClient.createVM({
184
- name: vmName,
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
- spinner.text = `Waiting for VM to be ready...`;
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!, existingNode.ipv6, appName);
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 --connect ssh:// to avoid local context dependency
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, primaryVmIp: string, contextName: string): Promise<void> {
333
+ private async joinUncloudCluster(newVmIp: string, appName: string): Promise<void> {
327
334
  try {
328
- // Get SSH certificates for both nodes and add to agent
329
- await this.sshCertManager.getSession(contextName, primaryVmIp);
330
- await this.sshCertManager.getSession(contextName, newVmIp);
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
  */