@flui-cloud/cli 0.0.1 → 0.1.0
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/lib/cli/src/commands/app/list.d.ts +3 -0
- package/lib/cli/src/commands/app/list.js +72 -18
- package/lib/cli/src/commands/app/status.d.ts +1 -0
- package/lib/cli/src/commands/app/status.js +27 -2
- package/lib/cli/src/commands/cluster/destroy.d.ts +1 -1
- package/lib/cli/src/commands/cluster/destroy.js +2 -2
- package/lib/cli/src/commands/deploy.d.ts +3 -0
- package/lib/cli/src/commands/deploy.js +19 -0
- package/lib/cli/src/commands/dev/creds.d.ts +0 -1
- package/lib/cli/src/commands/dev/creds.js +6 -27
- package/lib/cli/src/commands/dev/tunnel.js +8 -8
- package/lib/cli/src/commands/env/capacity.js +4 -4
- package/lib/cli/src/commands/env/create.d.ts +4 -1
- package/lib/cli/src/commands/env/create.js +73 -52
- package/lib/cli/src/commands/env/credentials.js +12 -12
- package/lib/cli/src/commands/env/destroy.d.ts +2 -1
- package/lib/cli/src/commands/env/destroy.js +45 -28
- package/lib/cli/src/commands/env/diag-ca.js +5 -5
- package/lib/cli/src/commands/env/export-config.d.ts +0 -17
- package/lib/cli/src/commands/env/export-config.js +45 -44
- package/lib/cli/src/commands/env/force-ready.d.ts +1 -1
- package/lib/cli/src/commands/env/force-ready.js +8 -8
- package/lib/cli/src/commands/env/inspect.js +5 -5
- package/lib/cli/src/commands/env/refresh-kubeconfig.js +4 -4
- package/lib/cli/src/commands/env/repair-ssh-ca.js +4 -4
- package/lib/cli/src/commands/env/repair-storage.d.ts +9 -0
- package/lib/cli/src/commands/env/repair-storage.js +82 -0
- package/lib/cli/src/commands/env/restart.d.ts +1 -1
- package/lib/cli/src/commands/env/restart.js +9 -9
- package/lib/cli/src/commands/env/scale-master.js +4 -4
- package/lib/cli/src/commands/env/scale-node.js +4 -4
- package/lib/cli/src/commands/env/set-master-protection.d.ts +16 -0
- package/lib/cli/src/commands/env/set-master-protection.js +120 -0
- package/lib/cli/src/commands/env/status.d.ts +1 -1
- package/lib/cli/src/commands/env/status.js +10 -10
- package/lib/cli/src/commands/env/stop.d.ts +1 -1
- package/lib/cli/src/commands/env/stop.js +8 -8
- package/lib/cli/src/commands/env/storage-expand.js +4 -4
- package/lib/cli/src/commands/env/storage.d.ts +1 -1
- package/lib/cli/src/commands/env/storage.js +5 -5
- package/lib/cli/src/commands/env/sync.js +5 -5
- package/lib/cli/src/commands/env/uncordon.js +4 -4
- package/lib/cli/src/commands/env/update-firewall.d.ts +13 -1
- package/lib/cli/src/commands/env/update-firewall.js +232 -126
- package/lib/cli/src/commands/integration/connect.d.ts +1 -0
- package/lib/cli/src/commands/integration/connect.js +19 -1
- package/lib/cli/src/commands/integration/reset.d.ts +13 -0
- package/lib/cli/src/commands/integration/reset.js +95 -0
- package/lib/cli/src/commands/integration/setup.d.ts +18 -0
- package/lib/cli/src/commands/integration/setup.js +320 -0
- package/lib/cli/src/commands/integration/status.d.ts +9 -0
- package/lib/cli/src/commands/integration/status.js +117 -0
- package/lib/cli/src/commands/node/list.d.ts +1 -0
- package/lib/cli/src/commands/node/list.js +19 -2
- package/lib/cli/src/commands/server-types/list.d.ts +3 -0
- package/lib/cli/src/commands/server-types/list.js +84 -0
- package/lib/cli/src/commands/ssh.js +5 -5
- package/lib/cli/src/commands/version.d.ts +18 -0
- package/lib/cli/src/commands/version.js +85 -0
- package/lib/cli/src/config/bootstrap.config.d.ts +10 -1
- package/lib/cli/src/config/bootstrap.config.js +21 -4
- package/lib/cli/src/config/preferences-schema.js +5 -5
- package/lib/cli/src/config/release.config.d.ts +31 -0
- package/lib/cli/src/config/release.config.js +38 -0
- package/lib/cli/src/lib/prompts.d.ts +1 -6
- package/lib/cli/src/lib/prompts.js +33 -13
- package/lib/cli/src/lib/services/cli-app.service.d.ts +33 -0
- package/lib/cli/src/lib/services/cli-app.service.js +9 -0
- package/lib/cli/src/lib/services/reconciliation.service.js +1 -1
- package/lib/cli/src/lib/templates/firewall-rules.d.ts +2 -2
- package/lib/cli/src/lib/templates/firewall-rules.js +3 -3
- package/lib/cli/src/modules/cli-infrastructure.module.js +3 -3
- package/lib/cli/src/services/cli-cluster-creator.service.js +31 -6
- package/lib/cli/src/services/cli-clusters.service.d.ts +3 -3
- package/lib/cli/src/services/cli-clusters.service.js +57 -34
- package/lib/cli/src/services/cli-control-cluster.service.d.ts +129 -0
- package/lib/cli/src/services/cli-control-cluster.service.js +544 -0
- package/lib/cli/src/services/cli-endpoint-resolver.service.d.ts +1 -0
- package/lib/cli/src/services/cli-endpoint-resolver.service.js +8 -2
- package/lib/cli/src/services/cli-k3s-script.service.d.ts +8 -1
- package/lib/cli/src/services/cli-k3s-script.service.js +14 -6
- package/lib/src/config/release.config.d.ts +28 -0
- package/lib/src/config/release.config.js +35 -0
- package/lib/src/modules/applications/entities/application.entity.d.ts +13 -20
- package/lib/src/modules/applications/entities/application.entity.js +12 -0
- package/lib/src/modules/applications/enums/application-exposure.enum.d.ts +2 -1
- package/lib/src/modules/applications/enums/application-exposure.enum.js +1 -0
- package/lib/src/modules/applications/interfaces/source-config.interface.d.ts +1 -0
- package/lib/src/modules/infrastructure/clusters/entities/cluster.entity.d.ts +8 -2
- package/lib/src/modules/infrastructure/clusters/entities/cluster.entity.js +16 -1
- package/lib/src/modules/infrastructure/clusters/services/cluster-node-scaling.service.js +2 -2
- package/lib/src/modules/infrastructure/firewalls/templates/firewall-rules.template.d.ts +3 -2
- package/lib/src/modules/infrastructure/firewalls/templates/firewall-rules.template.js +11 -4
- package/lib/src/modules/infrastructure/shared/services/kubernetes.service.d.ts +26 -0
- package/lib/src/modules/infrastructure/shared/services/kubernetes.service.js +105 -8
- package/lib/src/modules/management/entities/provider-capabilities.entity.d.ts +2 -0
- package/lib/src/modules/providers/implementations/contabo/contabo-capabilities.service.js +2 -0
- package/lib/src/modules/providers/implementations/hetzner/hetzner-capabilities.service.js +3 -6
- package/lib/src/modules/providers/implementations/scaleway/scaleway-capabilities.service.js +2 -1
- package/lib/src/modules/providers/implementations/scaleway/scaleway-firewall.service.js +3 -1
- package/lib/src/modules/providers/implementations/scaleway/scaleway-provider.service.js +3 -1
- package/lib/src/modules/providers/interfaces/provider-capabilities.interface.d.ts +0 -2
- package/lib/src/modules/providers/services/hetzner-firewall.service.d.ts +1 -1
- package/lib/src/modules/providers/services/hetzner-firewall.service.js +2 -1
- package/oclif.manifest.json +1025 -678
- package/package.json +2 -2
|
@@ -9,7 +9,7 @@ import { EncryptionService } from '../../../src/modules/shared/encryption/servic
|
|
|
9
9
|
import { CliSshService } from './cli-ssh.service';
|
|
10
10
|
import { CliCaService } from './cli-ca.service';
|
|
11
11
|
import { ProviderFactory } from '../../../src/modules/providers/services/provider.factory';
|
|
12
|
-
import {
|
|
12
|
+
import { FirewallProviderFactory } from '../../../src/modules/providers/core/factories/firewall-provider.factory';
|
|
13
13
|
/**
|
|
14
14
|
* CLI Clusters Service
|
|
15
15
|
*
|
|
@@ -26,10 +26,10 @@ export declare class CliClustersService {
|
|
|
26
26
|
private readonly sshService;
|
|
27
27
|
private readonly caService;
|
|
28
28
|
private readonly providerFactory;
|
|
29
|
-
private readonly
|
|
29
|
+
private readonly firewallFactory;
|
|
30
30
|
private readonly infrastructureQueue;
|
|
31
31
|
private readonly logger;
|
|
32
|
-
constructor(clusterRepository: CliClusterRepository, operationRepository: CliOperationRepository, nodeRepository: CliNodeRepository, firewallRepository: CliFirewallRepository, encryptionService: EncryptionService, sshService: CliSshService, caService: CliCaService, providerFactory: ProviderFactory,
|
|
32
|
+
constructor(clusterRepository: CliClusterRepository, operationRepository: CliOperationRepository, nodeRepository: CliNodeRepository, firewallRepository: CliFirewallRepository, encryptionService: EncryptionService, sshService: CliSshService, caService: CliCaService, providerFactory: ProviderFactory, firewallFactory: FirewallProviderFactory, infrastructureQueue: any);
|
|
33
33
|
/**
|
|
34
34
|
* Create a new cluster
|
|
35
35
|
*/
|
|
@@ -63,7 +63,7 @@ const encryption_service_1 = require("../../../src/modules/shared/encryption/ser
|
|
|
63
63
|
const cli_ssh_service_1 = require("./cli-ssh.service");
|
|
64
64
|
const cli_ca_service_1 = require("./cli-ca.service");
|
|
65
65
|
const provider_factory_1 = require("../../../src/modules/providers/services/provider.factory");
|
|
66
|
-
const
|
|
66
|
+
const firewall_provider_factory_1 = require("../../../src/modules/providers/core/factories/firewall-provider.factory");
|
|
67
67
|
const fs = __importStar(require("node:fs"));
|
|
68
68
|
const path = __importStar(require("node:path"));
|
|
69
69
|
const os = __importStar(require("node:os"));
|
|
@@ -75,7 +75,7 @@ const os = __importStar(require("node:os"));
|
|
|
75
75
|
* Integrates SSH and CA management for secure server access.
|
|
76
76
|
*/
|
|
77
77
|
let CliClustersService = CliClustersService_1 = class CliClustersService {
|
|
78
|
-
constructor(clusterRepository, operationRepository, nodeRepository, firewallRepository, encryptionService, sshService, caService, providerFactory,
|
|
78
|
+
constructor(clusterRepository, operationRepository, nodeRepository, firewallRepository, encryptionService, sshService, caService, providerFactory, firewallFactory, infrastructureQueue) {
|
|
79
79
|
this.clusterRepository = clusterRepository;
|
|
80
80
|
this.operationRepository = operationRepository;
|
|
81
81
|
this.nodeRepository = nodeRepository;
|
|
@@ -84,7 +84,7 @@ let CliClustersService = CliClustersService_1 = class CliClustersService {
|
|
|
84
84
|
this.sshService = sshService;
|
|
85
85
|
this.caService = caService;
|
|
86
86
|
this.providerFactory = providerFactory;
|
|
87
|
-
this.
|
|
87
|
+
this.firewallFactory = firewallFactory;
|
|
88
88
|
this.infrastructureQueue = infrastructureQueue;
|
|
89
89
|
this.logger = new common_1.Logger(CliClustersService_1.name);
|
|
90
90
|
}
|
|
@@ -134,17 +134,17 @@ let CliClustersService = CliClustersService_1 = class CliClustersService {
|
|
|
134
134
|
const jwtSecret = this.generateSecurePassword(64);
|
|
135
135
|
const adminEmail = createClusterDto.metadata?.adminEmail || '';
|
|
136
136
|
const adminPassword = this.generateSecurePassword(24);
|
|
137
|
-
// Generate Zitadel secrets (only used by
|
|
137
|
+
// Generate Zitadel secrets (only used by control clusters)
|
|
138
138
|
const zitadelMasterkey = this.generateSecurePassword(32);
|
|
139
139
|
const zitadelDbAdminPassword = this.generateSecurePassword(32);
|
|
140
140
|
const zitadelDbUserPassword = this.generateSecurePassword(32);
|
|
141
141
|
const zitadelAdminTempPassword = this.generateSecurePassword(24);
|
|
142
142
|
// Get worker count from DTO
|
|
143
143
|
const workerCount = createClusterDto.workerCount;
|
|
144
|
-
// Determine cluster type from metadata (same logic as API)
|
|
144
|
+
// Determine cluster type from metadata (same logic as API; accept legacy flag)
|
|
145
145
|
const metadata = createClusterDto.metadata || {};
|
|
146
|
-
const clusterType = metadata.isObservabilityCluster
|
|
147
|
-
? cluster_entity_1.ClusterType.
|
|
146
|
+
const clusterType = metadata.isControlCluster || metadata.isObservabilityCluster
|
|
147
|
+
? cluster_entity_1.ClusterType.CONTROL
|
|
148
148
|
: cluster_entity_1.ClusterType.WORKLOAD;
|
|
149
149
|
const hostnameMode = createClusterDto.endpointHostnameMode ?? hostname_mode_enum_1.HostnameMode.IP;
|
|
150
150
|
let nipHostnameToken = null;
|
|
@@ -266,7 +266,7 @@ let CliClustersService = CliClustersService_1 = class CliClustersService {
|
|
|
266
266
|
if (!cluster) {
|
|
267
267
|
throw new Error(`Cluster ${id} not found`);
|
|
268
268
|
}
|
|
269
|
-
this.logger.log(`Deleting
|
|
269
|
+
this.logger.log(`Deleting control cluster ${id} (${cluster.name})`);
|
|
270
270
|
this.logger.log(`Cluster details: ${JSON.stringify({
|
|
271
271
|
id: cluster.id,
|
|
272
272
|
name: cluster.name,
|
|
@@ -367,14 +367,14 @@ let CliClustersService = CliClustersService_1 = class CliClustersService {
|
|
|
367
367
|
labels: s.labels?.map((l) => `${l.key}=${l.value}`),
|
|
368
368
|
})))}`);
|
|
369
369
|
}
|
|
370
|
-
// Filter servers that belong to this
|
|
370
|
+
// Filter servers that belong to this control cluster
|
|
371
371
|
const orphanedServers = allServers.filter((server) => {
|
|
372
372
|
// Check if already deleted in Phase 1
|
|
373
373
|
if (deletedServerIds.has(server.provider_resource_id)) {
|
|
374
374
|
this.logger.log(`Skipping server ${server.name} - already deleted in Phase 1`);
|
|
375
375
|
return false;
|
|
376
376
|
}
|
|
377
|
-
// Check if server belongs to this
|
|
377
|
+
// Check if server belongs to this control cluster
|
|
378
378
|
const hasObservabilityId = server.labels?.some((label) => label.key === 'flui-cluster-id' && label.value === cluster.id);
|
|
379
379
|
// Check if server is managed by flui
|
|
380
380
|
const isFluiManaged = server.labels?.some((label) => label.key === 'managed-by' && label.value === 'flui-cloud');
|
|
@@ -434,9 +434,11 @@ let CliClustersService = CliClustersService_1 = class CliClustersService {
|
|
|
434
434
|
// PHASE 2.5: Delete cluster firewalls (GUARANTEED CLEANUP)
|
|
435
435
|
this.logger.log(`Phase 2.5: Deleting cluster firewalls from ${cluster.provider}...`);
|
|
436
436
|
try {
|
|
437
|
+
const providerEnum = (cluster.provider || '').toLowerCase();
|
|
438
|
+
const firewallService = this.firewallFactory.getFirewallProviderOrFail(providerEnum);
|
|
437
439
|
// STEP 1: Find ALL firewalls for this cluster
|
|
438
440
|
this.logger.log(`Searching for firewalls with label flui-cluster-id=${cluster.id}`);
|
|
439
|
-
const clusterFirewalls = await
|
|
441
|
+
const clusterFirewalls = await firewallService.listFirewalls({
|
|
440
442
|
clusterId: cluster.id,
|
|
441
443
|
});
|
|
442
444
|
this.logger.log(`Found ${clusterFirewalls.length} firewall(s) for cluster ${cluster.id}`);
|
|
@@ -444,13 +446,12 @@ let CliClustersService = CliClustersService_1 = class CliClustersService {
|
|
|
444
446
|
let allFirewalls = [...clusterFirewalls];
|
|
445
447
|
if (clusterFirewalls.length === 0) {
|
|
446
448
|
this.logger.log(`No firewalls found by label. Searching by name pattern...`);
|
|
447
|
-
//
|
|
448
|
-
//
|
|
449
|
-
|
|
450
|
-
const
|
|
451
|
-
|
|
452
|
-
fw.name
|
|
453
|
-
fw.name.startsWith('flui-firewall-temp-'));
|
|
449
|
+
// Exact cluster-scoped names only — never global prefixes (would hit
|
|
450
|
+
// another cluster's firewall).
|
|
451
|
+
const allProviderFirewalls = await firewallService.listFirewalls({});
|
|
452
|
+
const nameMatchedFirewalls = allProviderFirewalls.filter((fw) => fw.name === `flui-control-firewall-${cluster.id}` ||
|
|
453
|
+
fw.name === `flui-control-${cluster.id}` ||
|
|
454
|
+
fw.name === `flui-observability-${cluster.id}`);
|
|
454
455
|
if (nameMatchedFirewalls.length > 0) {
|
|
455
456
|
this.logger.log(`Found ${nameMatchedFirewalls.length} firewall(s) by name pattern:`);
|
|
456
457
|
nameMatchedFirewalls.forEach((fw) => {
|
|
@@ -462,26 +463,45 @@ let CliClustersService = CliClustersService_1 = class CliClustersService {
|
|
|
462
463
|
this.logger.log(`No firewalls found with observability name patterns`);
|
|
463
464
|
}
|
|
464
465
|
}
|
|
466
|
+
// STEP 1c: Sweep orphaned control firewalls left with a TEMPORARY name.
|
|
467
|
+
// A firewall created before a failed cluster run keeps its temp name
|
|
468
|
+
// (`flui-control-firewall` or `flui-control-firewall-<hex>`) and has no
|
|
469
|
+
// flui-cluster-id label, so neither the label query nor the scoped-name
|
|
470
|
+
// fallback above can find it. There is exactly one control cluster per
|
|
471
|
+
// environment, so any such firewall is leftover garbage from a previous
|
|
472
|
+
// attempt and is safe to remove while tearing down the control cluster.
|
|
473
|
+
// The regex only matches the temp form (8-hex suffix), never another
|
|
474
|
+
// cluster's UUID-scoped name.
|
|
475
|
+
if ((0, cluster_entity_1.isControlClusterType)(cluster.clusterType)) {
|
|
476
|
+
const tempControlFirewall = /^flui-control-firewall(-[0-9a-f]{8})?$/;
|
|
477
|
+
const known = new Set(allFirewalls.map((fw) => fw.id));
|
|
478
|
+
const providerFirewalls = await firewallService.listFirewalls({});
|
|
479
|
+
const orphans = providerFirewalls.filter((fw) => tempControlFirewall.test(fw.name) && !known.has(fw.id));
|
|
480
|
+
if (orphans.length > 0) {
|
|
481
|
+
this.logger.log(`Found ${orphans.length} orphaned control firewall(s) with a temporary name:`);
|
|
482
|
+
orphans.forEach((fw) => this.logger.log(` - ${fw.name} (${fw.id})`));
|
|
483
|
+
allFirewalls.push(...orphans);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
465
486
|
if (allFirewalls.length === 0) {
|
|
466
487
|
this.logger.log('✓ No firewalls to delete for this cluster');
|
|
467
488
|
}
|
|
468
489
|
else {
|
|
469
|
-
// STEP 2: Delete each firewall
|
|
490
|
+
// STEP 2: Delete each firewall, retrying while the server still holds it
|
|
470
491
|
const deletePromises = allFirewalls.map(async (fw) => {
|
|
471
|
-
const maxRetries =
|
|
492
|
+
const maxRetries = 6;
|
|
472
493
|
const retryDelay = 5000; // 5 seconds
|
|
473
494
|
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
474
495
|
try {
|
|
475
496
|
this.logger.log(`Deleting firewall ${fw.name} (${fw.id}) - Attempt ${attempt}/${maxRetries}`);
|
|
476
|
-
await
|
|
497
|
+
await firewallService.deleteFirewall(fw.id);
|
|
477
498
|
this.logger.log(`✓ Deleted firewall ${fw.name}`);
|
|
478
499
|
return { success: true, id: fw.id, name: fw.name };
|
|
479
500
|
}
|
|
480
501
|
catch (error) {
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
this.logger.warn(`Firewall ${fw.name} is still in use. Waiting ${retryDelay}ms before retry ${attempt + 1}/${maxRetries}...`);
|
|
502
|
+
// Retry on any error: the firewall frees up once the server is gone.
|
|
503
|
+
if (attempt < maxRetries) {
|
|
504
|
+
this.logger.warn(`Firewall ${fw.name} not deletable yet (${error.message}); retry ${attempt + 1}/${maxRetries} in ${retryDelay}ms...`);
|
|
485
505
|
await new Promise((resolve) => setTimeout(resolve, retryDelay));
|
|
486
506
|
continue; // Retry
|
|
487
507
|
}
|
|
@@ -500,7 +520,7 @@ let CliClustersService = CliClustersService_1 = class CliClustersService {
|
|
|
500
520
|
const failedCount = results.filter((r) => !r.success).length;
|
|
501
521
|
// STEP 3: VERIFICATION - Re-scan to ensure all deleted
|
|
502
522
|
this.logger.log('Verifying all firewalls deleted...');
|
|
503
|
-
const remainingFirewalls = await
|
|
523
|
+
const remainingFirewalls = await firewallService.listFirewalls({
|
|
504
524
|
clusterId: cluster.id,
|
|
505
525
|
});
|
|
506
526
|
if (remainingFirewalls.length > 0) {
|
|
@@ -509,7 +529,7 @@ let CliClustersService = CliClustersService_1 = class CliClustersService {
|
|
|
509
529
|
for (const fw of remainingFirewalls) {
|
|
510
530
|
try {
|
|
511
531
|
this.logger.log(`RETRY: Deleting firewall ${fw.name} (${fw.id})`);
|
|
512
|
-
await
|
|
532
|
+
await firewallService.deleteFirewall(fw.id);
|
|
513
533
|
this.logger.log(`✓ RETRY SUCCESS: Deleted firewall ${fw.name}`);
|
|
514
534
|
}
|
|
515
535
|
catch (error) {
|
|
@@ -517,7 +537,7 @@ let CliClustersService = CliClustersService_1 = class CliClustersService {
|
|
|
517
537
|
}
|
|
518
538
|
}
|
|
519
539
|
// Final verification
|
|
520
|
-
const finalCheck = await
|
|
540
|
+
const finalCheck = await firewallService.listFirewalls({
|
|
521
541
|
clusterId: cluster.id,
|
|
522
542
|
});
|
|
523
543
|
if (finalCheck.length > 0) {
|
|
@@ -559,13 +579,16 @@ let CliClustersService = CliClustersService_1 = class CliClustersService {
|
|
|
559
579
|
if (provider.listSSHKeys && provider.deleteSSHKey) {
|
|
560
580
|
const allKeys = await provider.listSSHKeys();
|
|
561
581
|
this.logger.log(`Found ${allKeys.length} total SSH keys on provider`);
|
|
562
|
-
// Filter bootstrap keys for this
|
|
582
|
+
// Filter bootstrap keys for this control cluster
|
|
563
583
|
const bootstrapKeys = allKeys.filter((key) => {
|
|
564
584
|
const tags = key.tags || {};
|
|
565
|
-
const
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
585
|
+
const tagMatch = tags['managed-by'] === 'flui-cloud' &&
|
|
586
|
+
tags['flui-cluster-id'] === cluster.id &&
|
|
587
|
+
tags['flui-resource-type'] === 'ssh-key';
|
|
588
|
+
// Scaleway IAM keys have no tags — match the cluster id in the name.
|
|
589
|
+
const nameMatch = typeof key.name === 'string' &&
|
|
590
|
+
key.name.startsWith(`flui-bootstrap-${cluster.id}`);
|
|
591
|
+
return tagMatch || nameMatch;
|
|
569
592
|
});
|
|
570
593
|
this.logger.log(`Found ${bootstrapKeys.length} bootstrap SSH keys to clean up`);
|
|
571
594
|
// Delete bootstrap keys
|
|
@@ -758,5 +781,5 @@ exports.CliClustersService = CliClustersService = CliClustersService_1 = __decor
|
|
|
758
781
|
cli_ssh_service_1.CliSshService,
|
|
759
782
|
cli_ca_service_1.CliCaService,
|
|
760
783
|
provider_factory_1.ProviderFactory,
|
|
761
|
-
|
|
784
|
+
firewall_provider_factory_1.FirewallProviderFactory, Object])
|
|
762
785
|
], CliClustersService);
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { ClusterEntity } from '../../../src/modules/infrastructure/clusters/entities/cluster.entity';
|
|
2
|
+
import { CliClusterRepository } from '../lib/repositories/cli-cluster.repository';
|
|
3
|
+
import { CliNodeRepository } from '../lib/repositories/cli-node.repository';
|
|
4
|
+
import { CliClustersService } from './cli-clusters.service';
|
|
5
|
+
import { CliOperationRepository } from '../lib/repositories/cli-operation.repository';
|
|
6
|
+
import { CliSshService } from './cli-ssh.service';
|
|
7
|
+
import { InfrastructureOperationEntity } from '../../../src/modules/infrastructure/servers/entities/infrastructure-operations.entity';
|
|
8
|
+
/**
|
|
9
|
+
* CLI Control Cluster Service
|
|
10
|
+
*
|
|
11
|
+
* Simplified version of ObservabilityClusterService for CLI usage.
|
|
12
|
+
* Uses file-based repositories instead of TypeORM.
|
|
13
|
+
*/
|
|
14
|
+
export declare class CliControlClusterService {
|
|
15
|
+
private readonly clusterRepository;
|
|
16
|
+
private readonly nodeRepository;
|
|
17
|
+
private readonly clustersService;
|
|
18
|
+
private readonly operationRepository;
|
|
19
|
+
private readonly sshService;
|
|
20
|
+
private readonly logger;
|
|
21
|
+
constructor(clusterRepository: CliClusterRepository, nodeRepository: CliNodeRepository, clustersService: CliClustersService, operationRepository: CliOperationRepository, sshService: CliSshService);
|
|
22
|
+
/**
|
|
23
|
+
* Get the control cluster
|
|
24
|
+
*/
|
|
25
|
+
getControlCluster(): Promise<ClusterEntity | null>;
|
|
26
|
+
/**
|
|
27
|
+
* Check if control cluster exists
|
|
28
|
+
*/
|
|
29
|
+
hasControlCluster(): Promise<boolean>;
|
|
30
|
+
/**
|
|
31
|
+
* Get observability service endpoints
|
|
32
|
+
*/
|
|
33
|
+
getObservabilityEndpoints(clusterId: string): Promise<{
|
|
34
|
+
prometheus?: string;
|
|
35
|
+
grafana?: string;
|
|
36
|
+
loki?: string;
|
|
37
|
+
postgres?: string;
|
|
38
|
+
redis?: string;
|
|
39
|
+
fluiApi?: string;
|
|
40
|
+
fluiWeb?: string;
|
|
41
|
+
}>;
|
|
42
|
+
/**
|
|
43
|
+
* Create control cluster
|
|
44
|
+
*/
|
|
45
|
+
createControlCluster(provider: string, region: string, nodeSize: string, workerCount?: number, firewallId?: string, sourceCidrs?: string[], authMode?: string, envVnet?: {
|
|
46
|
+
vnetProviderResourceId: string;
|
|
47
|
+
vnetIpRange: string;
|
|
48
|
+
subnetProviderResourceId: string;
|
|
49
|
+
subnetIpRange: string;
|
|
50
|
+
subnetType: string;
|
|
51
|
+
networkZone: string;
|
|
52
|
+
}, adminEmail?: string, acmeStaging?: boolean, diskSizeGb?: number, options?: {
|
|
53
|
+
sharedStorageEnabled?: boolean;
|
|
54
|
+
sharedStorageVolumeSizeGb?: number;
|
|
55
|
+
useLatest?: boolean;
|
|
56
|
+
}): Promise<string>;
|
|
57
|
+
/**
|
|
58
|
+
* Deploy observability stack (Prometheus, Grafana, Loki)
|
|
59
|
+
*/
|
|
60
|
+
deployObservabilityStack(clusterId: string): Promise<void>;
|
|
61
|
+
/**
|
|
62
|
+
* Delete control cluster
|
|
63
|
+
*/
|
|
64
|
+
deleteControlCluster(): Promise<void>;
|
|
65
|
+
/**
|
|
66
|
+
* Get cluster operation by cluster ID
|
|
67
|
+
*/
|
|
68
|
+
getClusterOperation(clusterId: string): Promise<InfrastructureOperationEntity | null>;
|
|
69
|
+
/**
|
|
70
|
+
* Wait for cluster to be ready by polling operation status
|
|
71
|
+
* @param clusterId Cluster ID to wait for
|
|
72
|
+
* @param timeoutMs Timeout in milliseconds (default: 10 minutes)
|
|
73
|
+
* @param pollIntervalMs Polling interval in milliseconds (default: 10 seconds)
|
|
74
|
+
* @returns Promise that resolves when cluster is ready
|
|
75
|
+
* @throws Error if timeout is reached or operation fails
|
|
76
|
+
*/
|
|
77
|
+
waitForClusterReady(clusterId: string, timeoutMs?: number, pollIntervalMs?: number): Promise<void>;
|
|
78
|
+
/**
|
|
79
|
+
* Wait for master node IP address to become available
|
|
80
|
+
* Polls the cluster repository until masterIpAddress is populated
|
|
81
|
+
*/
|
|
82
|
+
waitForMasterIp(clusterId: string, timeoutMs?: number, pollIntervalMs?: number): Promise<string>;
|
|
83
|
+
/**
|
|
84
|
+
* Wait for TCP port to become reachable on a host
|
|
85
|
+
*/
|
|
86
|
+
waitForPortReady(host: string, port?: number, timeoutMs?: number, pollIntervalMs?: number): Promise<void>;
|
|
87
|
+
/**
|
|
88
|
+
* Wait for SSH to be fully ready (CA enrolled + cert auth working)
|
|
89
|
+
* Attempts an actual SSH command with retry until it succeeds.
|
|
90
|
+
* This is needed because TCP port 22 can be open before cloud-init
|
|
91
|
+
* has finished configuring the CA for certificate authentication.
|
|
92
|
+
*
|
|
93
|
+
* @param sshTestFn Function that attempts an SSH command and throws on failure
|
|
94
|
+
*/
|
|
95
|
+
waitForSshAuth(sshTestFn: () => Promise<void>, timeoutMs?: number, pollIntervalMs?: number): Promise<void>;
|
|
96
|
+
/**
|
|
97
|
+
* Poll operation status in background and call onReady when COMPLETED.
|
|
98
|
+
* Returns a stop function and a done promise that resolves when the
|
|
99
|
+
* poller finishes (after onReady/onFailed callback completes or stop() is called).
|
|
100
|
+
*/
|
|
101
|
+
pollOperationUntilReady(clusterId: string, onReady: () => Promise<void>, onFailed?: (error: string) => void, pollIntervalMs?: number): {
|
|
102
|
+
stop: () => void;
|
|
103
|
+
done: Promise<void>;
|
|
104
|
+
};
|
|
105
|
+
/**
|
|
106
|
+
* Check observability services health via HTTP endpoints
|
|
107
|
+
* @param masterIp Master node IP address
|
|
108
|
+
* @returns Object with service health status
|
|
109
|
+
*/
|
|
110
|
+
checkObservabilityServices(masterIp: string, _nipHostnameToken?: string | null): Promise<{
|
|
111
|
+
prometheus: 'healthy' | 'unreachable';
|
|
112
|
+
grafana: 'healthy' | 'unreachable';
|
|
113
|
+
loki: 'healthy' | 'unreachable';
|
|
114
|
+
postgres: 'healthy' | 'unreachable';
|
|
115
|
+
redis: 'healthy' | 'unreachable';
|
|
116
|
+
fluiApi: 'healthy' | 'unreachable';
|
|
117
|
+
fluiWeb: 'healthy' | 'unreachable';
|
|
118
|
+
}>;
|
|
119
|
+
/**
|
|
120
|
+
* Check if a TCP port is reachable on a host
|
|
121
|
+
*/
|
|
122
|
+
private checkTcpPort;
|
|
123
|
+
/**
|
|
124
|
+
* Sleep helper
|
|
125
|
+
*/
|
|
126
|
+
private sleep;
|
|
127
|
+
waitForValidTls(url: string, timeoutMs?: number, intervalMs?: number, acmeStaging?: boolean): Promise<boolean>;
|
|
128
|
+
waitForOidcReady(apiBaseUrl: string, timeoutMs?: number, intervalMs?: number, acmeStaging?: boolean): Promise<boolean>;
|
|
129
|
+
}
|