@flui-cloud/cli 0.0.1 → 0.2.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.
Files changed (108) hide show
  1. package/lib/cli/src/commands/app/list.d.ts +3 -0
  2. package/lib/cli/src/commands/app/list.js +72 -18
  3. package/lib/cli/src/commands/app/status.d.ts +1 -0
  4. package/lib/cli/src/commands/app/status.js +27 -2
  5. package/lib/cli/src/commands/cluster/destroy.d.ts +1 -1
  6. package/lib/cli/src/commands/cluster/destroy.js +2 -2
  7. package/lib/cli/src/commands/deploy.d.ts +3 -0
  8. package/lib/cli/src/commands/deploy.js +19 -0
  9. package/lib/cli/src/commands/dev/creds.d.ts +0 -1
  10. package/lib/cli/src/commands/dev/creds.js +6 -27
  11. package/lib/cli/src/commands/dev/tunnel.js +8 -8
  12. package/lib/cli/src/commands/env/capacity.js +4 -4
  13. package/lib/cli/src/commands/env/create.d.ts +4 -1
  14. package/lib/cli/src/commands/env/create.js +78 -52
  15. package/lib/cli/src/commands/env/credentials.js +12 -12
  16. package/lib/cli/src/commands/env/destroy.d.ts +2 -1
  17. package/lib/cli/src/commands/env/destroy.js +45 -28
  18. package/lib/cli/src/commands/env/diag-ca.js +5 -5
  19. package/lib/cli/src/commands/env/export-config.d.ts +0 -17
  20. package/lib/cli/src/commands/env/export-config.js +50 -47
  21. package/lib/cli/src/commands/env/force-ready.d.ts +1 -1
  22. package/lib/cli/src/commands/env/force-ready.js +8 -8
  23. package/lib/cli/src/commands/env/inspect.js +5 -5
  24. package/lib/cli/src/commands/env/refresh-kubeconfig.js +4 -4
  25. package/lib/cli/src/commands/env/repair-ssh-ca.js +4 -4
  26. package/lib/cli/src/commands/env/repair-storage.d.ts +9 -0
  27. package/lib/cli/src/commands/env/repair-storage.js +82 -0
  28. package/lib/cli/src/commands/env/restart.d.ts +1 -1
  29. package/lib/cli/src/commands/env/restart.js +9 -9
  30. package/lib/cli/src/commands/env/scale-master.js +4 -4
  31. package/lib/cli/src/commands/env/scale-node.js +4 -4
  32. package/lib/cli/src/commands/env/set-master-protection.d.ts +16 -0
  33. package/lib/cli/src/commands/env/set-master-protection.js +120 -0
  34. package/lib/cli/src/commands/env/status.d.ts +1 -1
  35. package/lib/cli/src/commands/env/status.js +10 -10
  36. package/lib/cli/src/commands/env/stop.d.ts +1 -1
  37. package/lib/cli/src/commands/env/stop.js +8 -8
  38. package/lib/cli/src/commands/env/storage-expand.js +4 -4
  39. package/lib/cli/src/commands/env/storage.d.ts +1 -1
  40. package/lib/cli/src/commands/env/storage.js +5 -5
  41. package/lib/cli/src/commands/env/sync.js +5 -5
  42. package/lib/cli/src/commands/env/uncordon.js +4 -4
  43. package/lib/cli/src/commands/env/update-firewall.d.ts +13 -1
  44. package/lib/cli/src/commands/env/update-firewall.js +232 -126
  45. package/lib/cli/src/commands/integration/connect.d.ts +1 -0
  46. package/lib/cli/src/commands/integration/connect.js +19 -1
  47. package/lib/cli/src/commands/integration/reset.d.ts +13 -0
  48. package/lib/cli/src/commands/integration/reset.js +95 -0
  49. package/lib/cli/src/commands/integration/setup.d.ts +18 -0
  50. package/lib/cli/src/commands/integration/setup.js +320 -0
  51. package/lib/cli/src/commands/integration/status.d.ts +9 -0
  52. package/lib/cli/src/commands/integration/status.js +117 -0
  53. package/lib/cli/src/commands/node/list.d.ts +1 -0
  54. package/lib/cli/src/commands/node/list.js +19 -2
  55. package/lib/cli/src/commands/server-types/list.d.ts +3 -0
  56. package/lib/cli/src/commands/server-types/list.js +84 -0
  57. package/lib/cli/src/commands/ssh.js +5 -5
  58. package/lib/cli/src/commands/version.d.ts +18 -0
  59. package/lib/cli/src/commands/version.js +100 -0
  60. package/lib/cli/src/config/bootstrap.config.d.ts +10 -1
  61. package/lib/cli/src/config/bootstrap.config.js +24 -4
  62. package/lib/cli/src/config/preferences-schema.js +5 -5
  63. package/lib/cli/src/config/release-override.d.ts +43 -0
  64. package/lib/cli/src/config/release-override.js +203 -0
  65. package/lib/cli/src/config/release.config.d.ts +31 -0
  66. package/lib/cli/src/config/release.config.js +38 -0
  67. package/lib/cli/src/lib/prompts.d.ts +1 -6
  68. package/lib/cli/src/lib/prompts.js +33 -13
  69. package/lib/cli/src/lib/services/cli-app.service.d.ts +33 -0
  70. package/lib/cli/src/lib/services/cli-app.service.js +9 -0
  71. package/lib/cli/src/lib/services/reconciliation.service.js +1 -1
  72. package/lib/cli/src/lib/templates/firewall-rules.d.ts +2 -2
  73. package/lib/cli/src/lib/templates/firewall-rules.js +3 -3
  74. package/lib/cli/src/modules/cli-infrastructure.module.js +3 -3
  75. package/lib/cli/src/services/cli-cluster-creator.service.js +31 -6
  76. package/lib/cli/src/services/cli-clusters.service.d.ts +3 -3
  77. package/lib/cli/src/services/cli-clusters.service.js +57 -34
  78. package/lib/cli/src/services/cli-control-cluster.service.d.ts +129 -0
  79. package/lib/cli/src/services/cli-control-cluster.service.js +545 -0
  80. package/lib/cli/src/services/cli-endpoint-resolver.service.d.ts +1 -0
  81. package/lib/cli/src/services/cli-endpoint-resolver.service.js +25 -11
  82. package/lib/cli/src/services/cli-k3s-script.service.d.ts +8 -1
  83. package/lib/cli/src/services/cli-k3s-script.service.js +14 -6
  84. package/lib/src/config/release.config.d.ts +28 -0
  85. package/lib/src/config/release.config.js +35 -0
  86. package/lib/src/modules/applications/entities/application.entity.d.ts +13 -20
  87. package/lib/src/modules/applications/entities/application.entity.js +12 -0
  88. package/lib/src/modules/applications/enums/application-exposure.enum.d.ts +2 -1
  89. package/lib/src/modules/applications/enums/application-exposure.enum.js +1 -0
  90. package/lib/src/modules/applications/interfaces/source-config.interface.d.ts +1 -0
  91. package/lib/src/modules/infrastructure/clusters/entities/cluster.entity.d.ts +8 -2
  92. package/lib/src/modules/infrastructure/clusters/entities/cluster.entity.js +16 -1
  93. package/lib/src/modules/infrastructure/clusters/services/cluster-node-scaling.service.js +2 -2
  94. package/lib/src/modules/infrastructure/firewalls/templates/firewall-rules.template.d.ts +3 -2
  95. package/lib/src/modules/infrastructure/firewalls/templates/firewall-rules.template.js +11 -4
  96. package/lib/src/modules/infrastructure/shared/services/kubernetes.service.d.ts +26 -0
  97. package/lib/src/modules/infrastructure/shared/services/kubernetes.service.js +105 -8
  98. package/lib/src/modules/management/entities/provider-capabilities.entity.d.ts +2 -0
  99. package/lib/src/modules/providers/implementations/contabo/contabo-capabilities.service.js +2 -0
  100. package/lib/src/modules/providers/implementations/hetzner/hetzner-capabilities.service.js +3 -6
  101. package/lib/src/modules/providers/implementations/scaleway/scaleway-capabilities.service.js +2 -1
  102. package/lib/src/modules/providers/implementations/scaleway/scaleway-firewall.service.js +3 -1
  103. package/lib/src/modules/providers/implementations/scaleway/scaleway-provider.service.js +3 -1
  104. package/lib/src/modules/providers/interfaces/provider-capabilities.interface.d.ts +0 -2
  105. package/lib/src/modules/providers/services/hetzner-firewall.service.d.ts +1 -1
  106. package/lib/src/modules/providers/services/hetzner-firewall.service.js +2 -1
  107. package/oclif.manifest.json +1201 -854
  108. package/package.json +2 -2
@@ -10,6 +10,24 @@ export interface AppSummary {
10
10
  lastDeployedAt?: string;
11
11
  clusterId: string;
12
12
  }
13
+ export interface AppGroupComponent extends AppSummary {
14
+ isPrimary?: boolean;
15
+ }
16
+ export interface AppGroup {
17
+ id: string;
18
+ type: 'standalone' | 'composed';
19
+ name: string;
20
+ slug: string;
21
+ status: string;
22
+ category: string;
23
+ clusterId: string;
24
+ url?: string;
25
+ catalogSlug?: string;
26
+ catalogInstallId?: string;
27
+ primaryComponentId?: string;
28
+ componentCount: number;
29
+ components: AppGroupComponent[];
30
+ }
13
31
  export interface AppRuntime {
14
32
  appId: string;
15
33
  deploymentName: string;
@@ -186,14 +204,29 @@ export interface AvailableVersionsResponse {
186
204
  nextPage: number | null;
187
205
  allowedPatterns: string[] | null;
188
206
  }
207
+ export interface AppEndpoint {
208
+ id: string;
209
+ clusterId: string;
210
+ applicationId?: string;
211
+ endpointType: string;
212
+ hostnameMode: string;
213
+ fqdn: string;
214
+ tlsEnabled: boolean;
215
+ certificateStatus?: string;
216
+ certificateMessage?: string;
217
+ reconciliationStatus?: string;
218
+ errorMessage?: string;
219
+ }
189
220
  export declare class CliAppService {
190
221
  private readonly apiClient;
191
222
  private readonly clusterId;
192
223
  constructor(apiClient: ApiClient, clusterId: string);
193
224
  static create(clusterId: string): Promise<CliAppService>;
194
225
  listApps(): Promise<AppSummary[]>;
226
+ listAppGroups(): Promise<AppGroup[]>;
195
227
  getAppByName(name: string): Promise<AppSummary>;
196
228
  getRuntime(appId: string): Promise<AppRuntime>;
229
+ listEndpoints(applicationId?: string): Promise<AppEndpoint[]>;
197
230
  getLogs(options: AppLogsOptions): Promise<AppLogsResponse>;
198
231
  scale(appId: string, replicas: number): Promise<AppRuntime>;
199
232
  restart(appId: string): Promise<void>;
@@ -21,6 +21,9 @@ class CliAppService {
21
21
  async listApps() {
22
22
  return this.apiClient.get(`/clusters/${this.clusterId}/applications`);
23
23
  }
24
+ async listAppGroups() {
25
+ return this.apiClient.get(`/clusters/${this.clusterId}/applications/grouped`);
26
+ }
24
27
  async getAppByName(name) {
25
28
  const apps = await this.listApps();
26
29
  const app = apps.find((a) => a.name.toLowerCase() === name.toLowerCase() ||
@@ -33,6 +36,12 @@ class CliAppService {
33
36
  async getRuntime(appId) {
34
37
  return this.apiClient.get(`/applications/${appId}/runtime`);
35
38
  }
39
+ async listEndpoints(applicationId) {
40
+ const all = await this.apiClient.get(`/clusters/${this.clusterId}/endpoints`);
41
+ return applicationId
42
+ ? all.filter((e) => e.applicationId === applicationId)
43
+ : all;
44
+ }
36
45
  async getLogs(options) {
37
46
  const params = new URLSearchParams();
38
47
  if (options.app)
@@ -72,7 +72,7 @@ class ReconciliationService {
72
72
  return {
73
73
  type: ReconciliationType.DNS,
74
74
  success: true,
75
- message: 'No observability cluster found, skipping DNS reconciliation',
75
+ message: 'No control cluster found, skipping DNS reconciliation',
76
76
  };
77
77
  }
78
78
  try {
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * @deprecated Import from shared template instead:
3
- * import { OBSERVABILITY_FIREWALL_RULES, OBSERVABILITY_PORTS } from '../../../../src/modules/infrastructure/firewalls/templates/firewall-rules.template';
3
+ * import { CONTROL_FIREWALL_RULES, OBSERVABILITY_PORTS } from '../../../../src/modules/infrastructure/firewalls/templates/firewall-rules.template';
4
4
  *
5
5
  * This file is kept for backward compatibility with existing CLI code.
6
6
  * New code should use the shared template.
7
7
  */
8
- export { OBSERVABILITY_FIREWALL_RULES, OBSERVABILITY_PORTS, WORKLOAD_FIREWALL_RULES, WORKLOAD_PORTS, validateFirewallRules, getFirewallRulesForClusterType, } from '../../../../src/modules/infrastructure/firewalls/templates/firewall-rules.template';
8
+ export { CONTROL_FIREWALL_RULES, OBSERVABILITY_PORTS, WORKLOAD_FIREWALL_RULES, WORKLOAD_PORTS, validateFirewallRules, getFirewallRulesForClusterType, } from '../../../../src/modules/infrastructure/firewalls/templates/firewall-rules.template';
@@ -1,15 +1,15 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.getFirewallRulesForClusterType = exports.validateFirewallRules = exports.WORKLOAD_PORTS = exports.WORKLOAD_FIREWALL_RULES = exports.OBSERVABILITY_PORTS = exports.OBSERVABILITY_FIREWALL_RULES = void 0;
3
+ exports.getFirewallRulesForClusterType = exports.validateFirewallRules = exports.WORKLOAD_PORTS = exports.WORKLOAD_FIREWALL_RULES = exports.OBSERVABILITY_PORTS = exports.CONTROL_FIREWALL_RULES = void 0;
4
4
  /**
5
5
  * @deprecated Import from shared template instead:
6
- * import { OBSERVABILITY_FIREWALL_RULES, OBSERVABILITY_PORTS } from '../../../../src/modules/infrastructure/firewalls/templates/firewall-rules.template';
6
+ * import { CONTROL_FIREWALL_RULES, OBSERVABILITY_PORTS } from '../../../../src/modules/infrastructure/firewalls/templates/firewall-rules.template';
7
7
  *
8
8
  * This file is kept for backward compatibility with existing CLI code.
9
9
  * New code should use the shared template.
10
10
  */
11
11
  var firewall_rules_template_1 = require("../../../../src/modules/infrastructure/firewalls/templates/firewall-rules.template");
12
- Object.defineProperty(exports, "OBSERVABILITY_FIREWALL_RULES", { enumerable: true, get: function () { return firewall_rules_template_1.OBSERVABILITY_FIREWALL_RULES; } });
12
+ Object.defineProperty(exports, "CONTROL_FIREWALL_RULES", { enumerable: true, get: function () { return firewall_rules_template_1.CONTROL_FIREWALL_RULES; } });
13
13
  Object.defineProperty(exports, "OBSERVABILITY_PORTS", { enumerable: true, get: function () { return firewall_rules_template_1.OBSERVABILITY_PORTS; } });
14
14
  Object.defineProperty(exports, "WORKLOAD_FIREWALL_RULES", { enumerable: true, get: function () { return firewall_rules_template_1.WORKLOAD_FIREWALL_RULES; } });
15
15
  Object.defineProperty(exports, "WORKLOAD_PORTS", { enumerable: true, get: function () { return firewall_rules_template_1.WORKLOAD_PORTS; } });
@@ -58,7 +58,7 @@ const label_service_1 = require("../../../src/modules/infrastructure/shared/serv
58
58
  const cli_k3s_script_service_1 = require("../services/cli-k3s-script.service");
59
59
  const cli_cluster_creator_service_1 = require("../services/cli-cluster-creator.service");
60
60
  const cli_clusters_service_1 = require("../services/cli-clusters.service");
61
- const cli_observability_cluster_service_1 = require("../services/cli-observability-cluster.service");
61
+ const cli_control_cluster_service_1 = require("../services/cli-control-cluster.service");
62
62
  const cli_ssh_service_1 = require("../services/cli-ssh.service");
63
63
  const cli_ca_service_1 = require("../services/cli-ca.service");
64
64
  const cli_logger_service_1 = require("../services/cli-logger.service");
@@ -174,7 +174,7 @@ exports.CliInfrastructureModule = CliInfrastructureModule = __decorate([
174
174
  label_service_1.LabelService,
175
175
  kubernetes_service_1.KubernetesService,
176
176
  cli_clusters_service_1.CliClustersService,
177
- cli_observability_cluster_service_1.CliObservabilityClusterService,
177
+ cli_control_cluster_service_1.CliControlClusterService,
178
178
  cli_ssh_service_1.CliSshService,
179
179
  cli_ca_service_1.CliCaService,
180
180
  cli_logger_service_1.CliLoggerService,
@@ -233,7 +233,7 @@ exports.CliInfrastructureModule = CliInfrastructureModule = __decorate([
233
233
  cluster_node_scaling_service_1.ClusterNodeScalingService,
234
234
  ],
235
235
  exports: [
236
- cli_observability_cluster_service_1.CliObservabilityClusterService,
236
+ cli_control_cluster_service_1.CliControlClusterService,
237
237
  cli_clusters_service_1.CliClustersService,
238
238
  kubernetes_service_1.KubernetesService,
239
239
  label_service_1.LabelService,
@@ -61,6 +61,7 @@ const node_child_process_1 = require("node:child_process");
61
61
  const fs = __importStar(require("node:fs"));
62
62
  const path = __importStar(require("node:path"));
63
63
  const os = __importStar(require("node:os"));
64
+ const profile_manager_1 = require("../lib/profile-manager");
64
65
  const https = __importStar(require("node:https"));
65
66
  const hetzner_firewall_service_1 = require("../../../src/modules/providers/services/hetzner-firewall.service");
66
67
  const cli_firewall_repository_1 = require("../lib/repositories/cli-firewall.repository");
@@ -167,7 +168,7 @@ let CliClusterCreatorService = CliClusterCreatorService_1 = class CliClusterCrea
167
168
  provider: cluster.provider,
168
169
  caPublicKey,
169
170
  operationId: operation.id,
170
- deployObservabilityStack: cluster.clusterType === cluster_entity_1.ClusterType.OBSERVABILITY,
171
+ deployObservabilityStack: (0, cluster_entity_1.isControlClusterType)(cluster.clusterType),
171
172
  postgresPassword,
172
173
  redisPassword,
173
174
  grafanaPassword,
@@ -190,6 +191,7 @@ let CliClusterCreatorService = CliClusterCreatorService_1 = class CliClusterCrea
190
191
  clusterFirewallId: firewallId || '',
191
192
  nipIoCertEnabled: !clusterMeta?.zitadelDomain,
192
193
  acmeStaging: !!clusterMeta?.acmeStaging,
194
+ useLatest: !!clusterMeta?.useLatest,
193
195
  nipHostnameToken: cluster.nipHostnameToken || null,
194
196
  envVnet: envVnet
195
197
  ? {
@@ -277,7 +279,7 @@ let CliClusterCreatorService = CliClusterCreatorService_1 = class CliClusterCrea
277
279
  sharedStorageVolumeSizeGb: cluster.sharedStorageVolumeSizeGb ?? undefined,
278
280
  });
279
281
  // Step 3c: Informational — Zitadel PAT injected on demand via sync-auth-domain
280
- if (cluster.clusterType === cluster_entity_1.ClusterType.OBSERVABILITY) {
282
+ if ((0, cluster_entity_1.isControlClusterType)(cluster.clusterType)) {
281
283
  this.log(opId, 'ℹ️ Zitadel service account PAT will be injected when sync-auth-domain is called.');
282
284
  this.log(opId, ' After DNS is configured, call: POST /api/v1/clusters/:id/dns-zone/sync-auth-domain');
283
285
  }
@@ -332,6 +334,7 @@ let CliClusterCreatorService = CliClusterCreatorService_1 = class CliClusterCrea
332
334
  provider: cluster.provider,
333
335
  caPublicKey,
334
336
  operationId: operation.id,
337
+ useLatest: !!clusterMeta?.useLatest,
335
338
  sharedStorage: workerSharedStorage,
336
339
  });
337
340
  const workerServer = await provider.createServer({
@@ -362,12 +365,15 @@ let CliClusterCreatorService = CliClusterCreatorService_1 = class CliClusterCrea
362
365
  try {
363
366
  const firewallRecord = await this.firewallRepository.findById(firewallId);
364
367
  if (firewallRecord) {
368
+ // Cluster-scoped name so destroy matches by exact name only.
369
+ const scopedFirewallName = `flui-control-firewall-${cluster.id}`;
365
370
  firewallRecord.clusterId = cluster.id;
371
+ firewallRecord.name = scopedFirewallName;
366
372
  // Get server IDs from cluster nodes
367
373
  const serverIds = cluster.nodes.map((node) => node.providerResourceId);
368
374
  firewallRecord.appliedToServerIds = serverIds;
369
375
  await this.firewallRepository.save(firewallRecord);
370
- // Update Hetzner firewall labels with cluster ID
376
+ // Update Hetzner firewall labels + name with cluster ID
371
377
  try {
372
378
  const existingLabels = Object.fromEntries(firewallRecord.labels.map((l) => [l.key, l.value]));
373
379
  const updatedLabels = {
@@ -375,7 +381,7 @@ let CliClusterCreatorService = CliClusterCreatorService_1 = class CliClusterCrea
375
381
  'flui-cluster-id': cluster.id,
376
382
  'flui-cluster-name': cluster.name,
377
383
  };
378
- await this.firewallService.updateFirewallLabels(firewallId, updatedLabels);
384
+ await this.firewallService.updateFirewallLabels(firewallId, updatedLabels, scopedFirewallName);
379
385
  this.log(opId, `✅ Firewall ${firewallId} labels updated on Hetzner with cluster ID`);
380
386
  }
381
387
  catch (labelError) {
@@ -399,6 +405,23 @@ let CliClusterCreatorService = CliClusterCreatorService_1 = class CliClusterCrea
399
405
  }
400
406
  catch (error) {
401
407
  this.log(opId, `Failed to create cluster: ${error.message}`, 'ERROR');
408
+ // Roll back the pre-created firewall. If creation failed before the
409
+ // rename/label step it stays with a temporary name and no
410
+ // flui-cluster-id label, which makes it invisible to `env destroy`
411
+ // (it matches neither the label query nor the scoped-name fallback)
412
+ // and leaks as an orphan that blocks the next run's name.
413
+ const orphanFirewallId = operation.metadata?.firewallId;
414
+ if (orphanFirewallId) {
415
+ try {
416
+ await this.firewallService.deleteFirewall(orphanFirewallId);
417
+ await this.firewallRepository.delete(orphanFirewallId);
418
+ this.log(opId, `✅ Rolled back firewall ${orphanFirewallId} after failed cluster creation`);
419
+ }
420
+ catch (cleanupError) {
421
+ this.log(opId, `Failed to roll back firewall ${orphanFirewallId}: ${cleanupError.message}. ` +
422
+ `Delete it manually on the provider to avoid an orphan.`, 'WARN');
423
+ }
424
+ }
402
425
  // Mark cluster as FAILED
403
426
  cluster.status = cluster_entity_1.ClusterStatus.ERROR;
404
427
  await this.clusterRepository.save(cluster);
@@ -571,8 +594,10 @@ let CliClusterCreatorService = CliClusterCreatorService_1 = class CliClusterCrea
571
594
  async createApiCredentialsSecret(operationId, masterIp, bootstrap) {
572
595
  this.log(operationId, 'Patching Kubernetes secret with SSH CA keys + bootstrap vars...');
573
596
  try {
574
- // Load CLI CA keys to share with API for unified SSH access
575
- const caKeyDir = path.join(os.homedir(), '.flui', 'ca');
597
+ // Load CLI CA keys to share with API for unified SSH access.
598
+ // CA keys are profile-scoped (~/.flui/profiles/<profile>/ca), not global —
599
+ // the old global path threw ENOENT and silently skipped the whole patch.
600
+ const caKeyDir = path.join(profile_manager_1.ProfileManager.getProfileDir(), 'ca');
576
601
  const caPrivateKey = fs
577
602
  .readFileSync(path.join(caKeyDir, 'ca_key'), 'utf8')
578
603
  .trim();
@@ -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 { HetznerFirewallService } from '../../../src/modules/providers/services/hetzner-firewall.service';
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 firewallService;
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, firewallService: HetznerFirewallService, infrastructureQueue: any);
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 hetzner_firewall_service_1 = require("../../../src/modules/providers/services/hetzner-firewall.service");
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, firewallService, infrastructureQueue) {
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.firewallService = firewallService;
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 OBSERVABILITY clusters)
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.OBSERVABILITY
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 observability cluster ${id} (${cluster.name})`);
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 observability cluster
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 observability cluster
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 this.firewallService.listFirewalls({
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
- // Search for firewalls with observability pattern:
448
- // - Old pattern: flui-observability-{clusterId}
449
- // - New pattern: flui-observability-firewall-{timestamp}
450
- const allProviderFirewalls = await this.firewallService.listFirewalls({});
451
- const nameMatchedFirewalls = allProviderFirewalls.filter((fw) => fw.name === `flui-observability-${cluster.id}` ||
452
- fw.name.startsWith('flui-observability-firewall-') ||
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 with retry logic for "resource_in_use" errors
490
+ // STEP 2: Delete each firewall, retrying while the server still holds it
470
491
  const deletePromises = allFirewalls.map(async (fw) => {
471
- const maxRetries = 5;
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 this.firewallService.deleteFirewall(fw.id);
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
- const isResourceInUse = error.message?.includes('resource_in_use') ||
482
- error.message?.includes('still in use');
483
- if (isResourceInUse && attempt < maxRetries) {
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 this.firewallService.listFirewalls({
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 this.firewallService.deleteFirewall(fw.id);
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 this.firewallService.listFirewalls({
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 observability cluster
582
+ // Filter bootstrap keys for this control cluster
563
583
  const bootstrapKeys = allKeys.filter((key) => {
564
584
  const tags = key.tags || {};
565
- const isFluiManaged = tags['managed-by'] === 'flui-cloud';
566
- const hasMatchingObsId = tags['flui-cluster-id'] === cluster.id;
567
- const isBootstrapKey = tags['flui-resource-type'] === 'ssh-key';
568
- return isFluiManaged && hasMatchingObsId && isBootstrapKey;
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
- hetzner_firewall_service_1.HetznerFirewallService, Object])
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
+ }