@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.
Files changed (106) 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 +73 -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 +45 -44
  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 +85 -0
  60. package/lib/cli/src/config/bootstrap.config.d.ts +10 -1
  61. package/lib/cli/src/config/bootstrap.config.js +21 -4
  62. package/lib/cli/src/config/preferences-schema.js +5 -5
  63. package/lib/cli/src/config/release.config.d.ts +31 -0
  64. package/lib/cli/src/config/release.config.js +38 -0
  65. package/lib/cli/src/lib/prompts.d.ts +1 -6
  66. package/lib/cli/src/lib/prompts.js +33 -13
  67. package/lib/cli/src/lib/services/cli-app.service.d.ts +33 -0
  68. package/lib/cli/src/lib/services/cli-app.service.js +9 -0
  69. package/lib/cli/src/lib/services/reconciliation.service.js +1 -1
  70. package/lib/cli/src/lib/templates/firewall-rules.d.ts +2 -2
  71. package/lib/cli/src/lib/templates/firewall-rules.js +3 -3
  72. package/lib/cli/src/modules/cli-infrastructure.module.js +3 -3
  73. package/lib/cli/src/services/cli-cluster-creator.service.js +31 -6
  74. package/lib/cli/src/services/cli-clusters.service.d.ts +3 -3
  75. package/lib/cli/src/services/cli-clusters.service.js +57 -34
  76. package/lib/cli/src/services/cli-control-cluster.service.d.ts +129 -0
  77. package/lib/cli/src/services/cli-control-cluster.service.js +544 -0
  78. package/lib/cli/src/services/cli-endpoint-resolver.service.d.ts +1 -0
  79. package/lib/cli/src/services/cli-endpoint-resolver.service.js +8 -2
  80. package/lib/cli/src/services/cli-k3s-script.service.d.ts +8 -1
  81. package/lib/cli/src/services/cli-k3s-script.service.js +14 -6
  82. package/lib/src/config/release.config.d.ts +28 -0
  83. package/lib/src/config/release.config.js +35 -0
  84. package/lib/src/modules/applications/entities/application.entity.d.ts +13 -20
  85. package/lib/src/modules/applications/entities/application.entity.js +12 -0
  86. package/lib/src/modules/applications/enums/application-exposure.enum.d.ts +2 -1
  87. package/lib/src/modules/applications/enums/application-exposure.enum.js +1 -0
  88. package/lib/src/modules/applications/interfaces/source-config.interface.d.ts +1 -0
  89. package/lib/src/modules/infrastructure/clusters/entities/cluster.entity.d.ts +8 -2
  90. package/lib/src/modules/infrastructure/clusters/entities/cluster.entity.js +16 -1
  91. package/lib/src/modules/infrastructure/clusters/services/cluster-node-scaling.service.js +2 -2
  92. package/lib/src/modules/infrastructure/firewalls/templates/firewall-rules.template.d.ts +3 -2
  93. package/lib/src/modules/infrastructure/firewalls/templates/firewall-rules.template.js +11 -4
  94. package/lib/src/modules/infrastructure/shared/services/kubernetes.service.d.ts +26 -0
  95. package/lib/src/modules/infrastructure/shared/services/kubernetes.service.js +105 -8
  96. package/lib/src/modules/management/entities/provider-capabilities.entity.d.ts +2 -0
  97. package/lib/src/modules/providers/implementations/contabo/contabo-capabilities.service.js +2 -0
  98. package/lib/src/modules/providers/implementations/hetzner/hetzner-capabilities.service.js +3 -6
  99. package/lib/src/modules/providers/implementations/scaleway/scaleway-capabilities.service.js +2 -1
  100. package/lib/src/modules/providers/implementations/scaleway/scaleway-firewall.service.js +3 -1
  101. package/lib/src/modules/providers/implementations/scaleway/scaleway-provider.service.js +3 -1
  102. package/lib/src/modules/providers/interfaces/provider-capabilities.interface.d.ts +0 -2
  103. package/lib/src/modules/providers/services/hetzner-firewall.service.d.ts +1 -1
  104. package/lib/src/modules/providers/services/hetzner-firewall.service.js +2 -1
  105. package/oclif.manifest.json +1025 -678
  106. package/package.json +2 -2
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Release manifest — single source of truth for the versions Flui pins at
3
+ * install time. Shared by the backend (API-driven cluster creation) AND the
4
+ * CLI (which imports it via the `src/*` path alias), so bumping a release means
5
+ * editing this one file. The bootstrap-scripts ref can still be overridden via
6
+ * BOOTSTRAP_SCRIPTS_URL.
7
+ */
8
+ export interface ComponentImageTags {
9
+ /** ghcr.io/flui-cloud/core */
10
+ fluiApi: string;
11
+ /** ghcr.io/flui-cloud/dashboard */
12
+ fluiWeb: string;
13
+ /** ghcr.io/flui-cloud/flui-authz */
14
+ fluiAuthz: string;
15
+ }
16
+ export interface ReleaseManifest {
17
+ /** Platform release version, recorded on the cluster at install. */
18
+ version: string;
19
+ /** Git ref (tag) on flui-cloud/bootstrap-scripts holding scripts + manifests. */
20
+ bootstrapRef: string;
21
+ /** Pinned Docker image tags, per Flui component. */
22
+ images: ComponentImageTags;
23
+ }
24
+ export declare const RELEASE: ReleaseManifest;
25
+ /** Bootstrap-scripts git ref to install from. `useLatest` → `master`. */
26
+ export declare function resolveBootstrapRef(useLatest: boolean): string;
27
+ /** Docker image tags to deploy. `useLatest` → all `latest`. */
28
+ export declare function resolveImageTags(useLatest: boolean): ComponentImageTags;
@@ -0,0 +1,35 @@
1
+ "use strict";
2
+ /**
3
+ * Release manifest — single source of truth for the versions Flui pins at
4
+ * install time. Shared by the backend (API-driven cluster creation) AND the
5
+ * CLI (which imports it via the `src/*` path alias), so bumping a release means
6
+ * editing this one file. The bootstrap-scripts ref can still be overridden via
7
+ * BOOTSTRAP_SCRIPTS_URL.
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.RELEASE = void 0;
11
+ exports.resolveBootstrapRef = resolveBootstrapRef;
12
+ exports.resolveImageTags = resolveImageTags;
13
+ exports.RELEASE = {
14
+ version: '0.5.0',
15
+ bootstrapRef: 'v0.5.0',
16
+ images: {
17
+ fluiApi: '0.5.0',
18
+ fluiWeb: '0.5.0',
19
+ fluiAuthz: '0.5.0',
20
+ },
21
+ };
22
+ const LATEST_BOOTSTRAP_REF = 'master';
23
+ const LATEST_IMAGE_TAGS = {
24
+ fluiApi: 'latest',
25
+ fluiWeb: 'latest',
26
+ fluiAuthz: 'latest',
27
+ };
28
+ /** Bootstrap-scripts git ref to install from. `useLatest` → `master`. */
29
+ function resolveBootstrapRef(useLatest) {
30
+ return useLatest ? LATEST_BOOTSTRAP_REF : exports.RELEASE.bootstrapRef;
31
+ }
32
+ /** Docker image tags to deploy. `useLatest` → all `latest`. */
33
+ function resolveImageTags(useLatest) {
34
+ return useLatest ? { ...LATEST_IMAGE_TAGS } : { ...exports.RELEASE.images };
35
+ }
@@ -33,6 +33,11 @@ export declare class ApplicationEntity {
33
33
  workloadKind: 'Deployment' | 'StatefulSet' | 'DaemonSet';
34
34
  replicas: number;
35
35
  port?: number;
36
+ portProtocol?: 'http' | 'tcp';
37
+ configFiles?: Array<{
38
+ path: string;
39
+ content: string;
40
+ }>;
36
41
  currentRevisionId?: string;
37
42
  imageRef?: string;
38
43
  startCommand?: string;
@@ -88,31 +93,19 @@ export declare class ApplicationEntity {
88
93
  frameworkConfirmed?: string;
89
94
  isFluiManaged: boolean;
90
95
  /**
91
- * Placement strategy for the app's PersistentVolumeClaims.
92
- *
93
- * - `shared` (default): pods can run anywhere; PVCs land on the cluster
94
- * flui-shared (NFS) layer. Safe for stateless apps and light file I/O.
95
- * - `dedicated`: pods pin to the master node so writes hit the backing
96
- * Volume directly (no NFS hop). Required for databases (Postgres,
97
- * MariaDB, MongoDB, …) where NFS breaks fsync/locking semantics.
98
- *
99
- * Source: catalog manifest `spec.persistence.scope`, defaulting to
100
- * `shared` when missing.
96
+ * `shared` (default): PVCs ride the cluster flui-shared (NFS) layer, pods run
97
+ * anywhere. `dedicated`: pod pins to a worker's local disk (no NFS hop) —
98
+ * required by databases where NFS breaks fsync/locking. Source: catalog
99
+ * `spec.persistence.scope`.
101
100
  */
102
101
  persistenceScope: 'shared' | 'dedicated';
103
102
  /**
104
- * Target node for `persistenceScope=dedicated` workloads.
105
- *
106
- * - `null` (default) → pinned to the master node via the standard
107
- * `node-role.kubernetes.io/control-plane` selector + tolerations.
108
- * - explicit kubernetes node name → pinned to that specific worker via
109
- * `kubernetes.io/hostname=<name>`. The node hosting at least one
110
- * dedicated app is considered "locked" by the cluster scaling
111
- * primitives (cannot be drained nor scale-downed).
112
- *
113
- * Ignored when `persistenceScope=shared`.
103
+ * Worker hosting a `dedicated` app, locked against drain/scale-down while it
104
+ * lives there. Null until the deploy auto-assigns the roomiest worker.
114
105
  */
115
106
  dedicatedNodeName?: string;
107
+ /** Let a `dedicated` app schedule on the master instead of a worker. */
108
+ allowMasterPlacement: boolean;
116
109
  createdAt: Date;
117
110
  updatedAt: Date;
118
111
  deletedAt?: Date;
@@ -150,6 +150,14 @@ __decorate([
150
150
  (0, typeorm_1.Column)({ type: 'int', nullable: true }),
151
151
  __metadata("design:type", Number)
152
152
  ], ApplicationEntity.prototype, "port", void 0);
153
+ __decorate([
154
+ (0, typeorm_1.Column)({ type: 'varchar', length: 8, nullable: true }),
155
+ __metadata("design:type", String)
156
+ ], ApplicationEntity.prototype, "portProtocol", void 0);
157
+ __decorate([
158
+ (0, typeorm_1.Column)({ type: 'json', nullable: true }),
159
+ __metadata("design:type", Array)
160
+ ], ApplicationEntity.prototype, "configFiles", void 0);
153
161
  __decorate([
154
162
  (0, typeorm_1.Column)({ type: 'uuid', nullable: true }),
155
163
  __metadata("design:type", String)
@@ -250,6 +258,10 @@ __decorate([
250
258
  (0, typeorm_1.Column)({ type: 'varchar', length: 253, nullable: true }),
251
259
  __metadata("design:type", String)
252
260
  ], ApplicationEntity.prototype, "dedicatedNodeName", void 0);
261
+ __decorate([
262
+ (0, typeorm_1.Column)({ default: false }),
263
+ __metadata("design:type", Boolean)
264
+ ], ApplicationEntity.prototype, "allowMasterPlacement", void 0);
253
265
  __decorate([
254
266
  (0, typeorm_1.CreateDateColumn)({ type: 'timestamptz' }),
255
267
  __metadata("design:type", Date)
@@ -1,4 +1,5 @@
1
1
  export declare enum ApplicationExposure {
2
2
  PUBLIC = "public",
3
- INTERNAL = "internal"
3
+ INTERNAL = "internal",
4
+ CLUSTER = "cluster"
4
5
  }
@@ -5,4 +5,5 @@ var ApplicationExposure;
5
5
  (function (ApplicationExposure) {
6
6
  ApplicationExposure["PUBLIC"] = "public";
7
7
  ApplicationExposure["INTERNAL"] = "internal";
8
+ ApplicationExposure["CLUSTER"] = "cluster";
8
9
  })(ApplicationExposure || (exports.ApplicationExposure = ApplicationExposure = {}));
@@ -123,6 +123,7 @@ export interface ApplicationHealthProbe {
123
123
  httpPath?: string;
124
124
  httpPort?: number;
125
125
  httpScheme?: 'HTTP' | 'HTTPS';
126
+ httpHeaders?: Record<string, string>;
126
127
  tcpPort?: number;
127
128
  execCommand?: string[];
128
129
  initialDelaySeconds?: number;
@@ -11,9 +11,15 @@ export declare enum ClusterStatus {
11
11
  DELETED = "deleted"
12
12
  }
13
13
  export declare enum ClusterType {
14
- OBSERVABILITY = "observability",
15
- WORKLOAD = "workload"
14
+ CONTROL = "control",
15
+ WORKLOAD = "workload",
16
+ /** @deprecated legacy value for the control cluster; kept for back-compat reads until all rows are migrated. */
17
+ OBSERVABILITY = "observability"
16
18
  }
19
+ /** Maps the legacy `observability` value forward to `control`; passes other values through. */
20
+ export declare function normalizeClusterType(value?: ClusterType | string | null): ClusterType;
21
+ /** True for the control cluster, accepting both the new and legacy enum values. */
22
+ export declare function isControlClusterType(value?: ClusterType | string | null): boolean;
17
23
  export declare class ClusterEntity {
18
24
  id: string;
19
25
  generateId(): void;
@@ -10,6 +10,8 @@ var __metadata = (this && this.__metadata) || function (k, v) {
10
10
  };
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
12
  exports.ClusterEntity = exports.ClusterType = exports.ClusterStatus = void 0;
13
+ exports.normalizeClusterType = normalizeClusterType;
14
+ exports.isControlClusterType = isControlClusterType;
13
15
  const typeorm_1 = require("typeorm");
14
16
  const uuid_1 = require("uuid");
15
17
  const cluster_node_entity_1 = require("./cluster-node.entity");
@@ -27,9 +29,22 @@ var ClusterStatus;
27
29
  })(ClusterStatus || (exports.ClusterStatus = ClusterStatus = {}));
28
30
  var ClusterType;
29
31
  (function (ClusterType) {
30
- ClusterType["OBSERVABILITY"] = "observability";
32
+ ClusterType["CONTROL"] = "control";
31
33
  ClusterType["WORKLOAD"] = "workload";
34
+ /** @deprecated legacy value for the control cluster; kept for back-compat reads until all rows are migrated. */
35
+ ClusterType["OBSERVABILITY"] = "observability";
32
36
  })(ClusterType || (exports.ClusterType = ClusterType = {}));
37
+ /** Maps the legacy `observability` value forward to `control`; passes other values through. */
38
+ function normalizeClusterType(value) {
39
+ if (value === ClusterType.OBSERVABILITY) {
40
+ return ClusterType.CONTROL;
41
+ }
42
+ return value ?? ClusterType.WORKLOAD;
43
+ }
44
+ /** True for the control cluster, accepting both the new and legacy enum values. */
45
+ function isControlClusterType(value) {
46
+ return value === ClusterType.CONTROL || value === ClusterType.OBSERVABILITY;
47
+ }
33
48
  let ClusterEntity = class ClusterEntity {
34
49
  generateId() {
35
50
  if (!this.id) {
@@ -380,8 +380,8 @@ let ClusterNodeScalingService = ClusterNodeScalingService_1 = class ClusterNodeS
380
380
  throw new common_1.BadRequestException({
381
381
  code: 'NODE_LOCKED_BY_DEDICATED_APPS',
382
382
  message: `Node ${node.serverName} hosts ${apps.length} dedicated workload(s) ` +
383
- `(${apps.map((a) => a.slug).join(', ')}). Move them with ` +
384
- `\`flui app placement <slug> --node <other>\` or delete the apps before removing this worker.`,
383
+ `(${apps.map((a) => a.slug).join(', ')}). Back up their data, then delete ` +
384
+ `or redeploy these apps before removing this worker.`,
385
385
  details: { affectedApps: apps.map((a) => a.slug) },
386
386
  });
387
387
  }
@@ -11,7 +11,7 @@ import { FirewallRule } from '../../../providers/interfaces/firewall-provider.in
11
11
  * 80 Traefik HTTP (api/app/auth via nip.io ingress)
12
12
  * 443 Traefik HTTPS
13
13
  */
14
- export declare const OBSERVABILITY_FIREWALL_RULES: (sourceCidrs: string[]) => FirewallRule[];
14
+ export declare const CONTROL_FIREWALL_RULES: (sourceCidrs: string[]) => FirewallRule[];
15
15
  export declare const WORKLOAD_FIREWALL_RULES: (sourceCidrs: string[]) => FirewallRule[];
16
16
  export declare const OBSERVABILITY_PORTS: {
17
17
  readonly SSH: {
@@ -51,4 +51,5 @@ export declare function validateFirewallRules(rules: FirewallRule[]): {
51
51
  valid: boolean;
52
52
  errors: string[];
53
53
  };
54
- export declare function getFirewallRulesForClusterType(clusterType: 'observability' | 'workload', sourceCidrs: string[]): FirewallRule[];
54
+ export declare function sanitizeApiServerFirewallRules(rules: FirewallRule[], subnetCidr: string): FirewallRule[];
55
+ export declare function getFirewallRulesForClusterType(clusterType: 'control' | 'observability' | 'workload', sourceCidrs: string[]): FirewallRule[];
@@ -1,7 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.WORKLOAD_PORTS = exports.OBSERVABILITY_PORTS = exports.WORKLOAD_FIREWALL_RULES = exports.OBSERVABILITY_FIREWALL_RULES = void 0;
3
+ exports.WORKLOAD_PORTS = exports.OBSERVABILITY_PORTS = exports.WORKLOAD_FIREWALL_RULES = exports.CONTROL_FIREWALL_RULES = void 0;
4
4
  exports.validateFirewallRules = validateFirewallRules;
5
+ exports.sanitizeApiServerFirewallRules = sanitizeApiServerFirewallRules;
5
6
  exports.getFirewallRulesForClusterType = getFirewallRulesForClusterType;
6
7
  /**
7
8
  * Firewall rules — public surface only.
@@ -15,7 +16,7 @@ exports.getFirewallRulesForClusterType = getFirewallRulesForClusterType;
15
16
  * 80 Traefik HTTP (api/app/auth via nip.io ingress)
16
17
  * 443 Traefik HTTPS
17
18
  */
18
- const OBSERVABILITY_FIREWALL_RULES = (sourceCidrs) => [
19
+ const CONTROL_FIREWALL_RULES = (sourceCidrs) => [
19
20
  {
20
21
  description: 'SSH access for server management',
21
22
  direction: 'in',
@@ -50,7 +51,7 @@ const OBSERVABILITY_FIREWALL_RULES = (sourceCidrs) => [
50
51
  destinationIps: ['0.0.0.0/0', '::/0'],
51
52
  },
52
53
  ];
53
- exports.OBSERVABILITY_FIREWALL_RULES = OBSERVABILITY_FIREWALL_RULES;
54
+ exports.CONTROL_FIREWALL_RULES = CONTROL_FIREWALL_RULES;
54
55
  const WORKLOAD_FIREWALL_RULES = (sourceCidrs) => [
55
56
  {
56
57
  description: 'SSH access for cluster management',
@@ -150,6 +151,11 @@ function validateFirewallRules(rules) {
150
151
  errors,
151
152
  };
152
153
  }
154
+ function sanitizeApiServerFirewallRules(rules, subnetCidr) {
155
+ return rules.map((rule) => rule.direction === 'in' && rule.protocol === 'tcp' && rule.port === '6443'
156
+ ? { ...rule, sourceIps: [subnetCidr] }
157
+ : rule);
158
+ }
153
159
  function isValidCidr(cidr) {
154
160
  const ipv4Regex = /^(\d{1,3}\.){3}\d{1,3}\/\d{1,2}$/;
155
161
  const ipv6Regex = /^([0-9a-fA-F:]+)\/\d{1,3}$/;
@@ -157,8 +163,9 @@ function isValidCidr(cidr) {
157
163
  }
158
164
  function getFirewallRulesForClusterType(clusterType, sourceCidrs) {
159
165
  switch (clusterType) {
166
+ case 'control':
160
167
  case 'observability':
161
- return (0, exports.OBSERVABILITY_FIREWALL_RULES)(sourceCidrs);
168
+ return (0, exports.CONTROL_FIREWALL_RULES)(sourceCidrs);
162
169
  case 'workload':
163
170
  return (0, exports.WORKLOAD_FIREWALL_RULES)(sourceCidrs);
164
171
  default:
@@ -249,6 +249,11 @@ export declare class KubernetesService {
249
249
  cordonNode(kubeconfigContent: string, nodeName: string): Promise<void>;
250
250
  uncordonNode(kubeconfigContent: string, nodeName: string): Promise<void>;
251
251
  private setNodeUnschedulable;
252
+ /**
253
+ * Add/remove the control-plane NoSchedule taint on master node(s) via the
254
+ * k8s API. Returns false when no control-plane node is found.
255
+ */
256
+ setControlPlaneTaint(kubeconfigContent: string, apply: boolean): Promise<boolean>;
252
257
  /**
253
258
  * Sum resource requests (CPU in millicores, memory in Mi) across all
254
259
  * containers in Running pods across all namespaces.
@@ -290,6 +295,27 @@ export declare class KubernetesService {
290
295
  memory: number;
291
296
  };
292
297
  } | null>;
298
+ /**
299
+ * Allocatable, requested and free resources for every READY worker node
300
+ * (nodes without the control-plane/master label). CPU in millicores, mem Mi.
301
+ */
302
+ listWorkerNodeCapacities(kubeconfigContent: string): Promise<Array<{
303
+ nodeName: string;
304
+ allocatable: {
305
+ cpu: number;
306
+ memory: number;
307
+ };
308
+ requested: {
309
+ cpu: number;
310
+ memory: number;
311
+ };
312
+ free: {
313
+ cpu: number;
314
+ memory: number;
315
+ };
316
+ }>>;
317
+ private isReadyWorker;
318
+ private sumPodRequestsOnNode;
293
319
  /**
294
320
  * Parse a Kubernetes CPU string to millicores.
295
321
  * Examples: "250m" → 250, "2" → 2000, "0.5" → 500
@@ -43,6 +43,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
43
43
  exports.KubernetesService = void 0;
44
44
  const common_1 = require("@nestjs/common");
45
45
  const k8s = __importStar(require("@kubernetes/client-node"));
46
+ const node_stream_1 = require("node:stream");
46
47
  let KubernetesService = KubernetesService_1 = class KubernetesService {
47
48
  constructor() {
48
49
  this.logger = new common_1.Logger(KubernetesService_1.name);
@@ -618,16 +619,20 @@ let KubernetesService = KubernetesService_1 = class KubernetesService {
618
619
  return new Promise((resolve, reject) => {
619
620
  let stdout = '';
620
621
  let stderr = '';
621
- exec
622
- .exec(namespace, pod.metadata.name, containerName, command, {
623
- write: (chunk) => {
624
- stdout += typeof chunk === 'string' ? chunk : chunk.toString();
622
+ const stdoutStream = new node_stream_1.Writable({
623
+ write(chunk, _enc, cb) {
624
+ stdout += chunk.toString();
625
+ cb();
625
626
  },
626
- }, {
627
- write: (chunk) => {
628
- stderr += typeof chunk === 'string' ? chunk : chunk.toString();
627
+ });
628
+ const stderrStream = new node_stream_1.Writable({
629
+ write(chunk, _enc, cb) {
630
+ stderr += chunk.toString();
631
+ cb();
629
632
  },
630
- }, null, false, (status) => {
633
+ });
634
+ exec
635
+ .exec(namespace, pod.metadata.name, containerName, command, stdoutStream, stderrStream, null, false, (status) => {
631
636
  if (status?.status === 'Success') {
632
637
  resolve(stdout);
633
638
  }
@@ -1039,6 +1044,41 @@ let KubernetesService = KubernetesService_1 = class KubernetesService {
1039
1044
  };
1040
1045
  await client.patch(patch, undefined, undefined, 'flui-api', undefined, k8s.PatchStrategy.MergePatch);
1041
1046
  }
1047
+ /**
1048
+ * Add/remove the control-plane NoSchedule taint on master node(s) via the
1049
+ * k8s API. Returns false when no control-plane node is found.
1050
+ */
1051
+ async setControlPlaneTaint(kubeconfigContent, apply) {
1052
+ const key = 'node-role.kubernetes.io/control-plane';
1053
+ const { coreApi } = this.getKubeClient(kubeconfigContent);
1054
+ const nodes = await coreApi.listNode();
1055
+ const masters = (nodes.items ?? []).filter((n) => {
1056
+ const labels = n.metadata?.labels ?? {};
1057
+ return ('node-role.kubernetes.io/control-plane' in labels ||
1058
+ 'node-role.kubernetes.io/master' in labels);
1059
+ });
1060
+ if (masters.length === 0)
1061
+ return false;
1062
+ const kc = this.loadKubeconfig(kubeconfigContent);
1063
+ const client = k8s.KubernetesObjectApi.makeApiClient(kc);
1064
+ for (const node of masters) {
1065
+ const name = node.metadata?.name;
1066
+ if (!name)
1067
+ continue;
1068
+ const without = (node.spec?.taints ?? []).filter((t) => t.key !== key);
1069
+ const taints = apply
1070
+ ? [...without, { key, effect: 'NoSchedule' }]
1071
+ : without;
1072
+ const patch = {
1073
+ apiVersion: 'v1',
1074
+ kind: 'Node',
1075
+ metadata: { name },
1076
+ spec: { taints },
1077
+ };
1078
+ await client.patch(patch, undefined, undefined, 'flui-api', undefined, k8s.PatchStrategy.MergePatch);
1079
+ }
1080
+ return true;
1081
+ }
1042
1082
  /**
1043
1083
  * Sum resource requests (CPU in millicores, memory in Mi) across all
1044
1084
  * containers in Running pods across all namespaces.
@@ -1118,6 +1158,63 @@ let KubernetesService = KubernetesService_1 = class KubernetesService {
1118
1158
  requested: { cpu: cpuReq, memory: memReq },
1119
1159
  };
1120
1160
  }
1161
+ /**
1162
+ * Allocatable, requested and free resources for every READY worker node
1163
+ * (nodes without the control-plane/master label). CPU in millicores, mem Mi.
1164
+ */
1165
+ async listWorkerNodeCapacities(kubeconfigContent) {
1166
+ const { coreApi } = this.getKubeClient(kubeconfigContent);
1167
+ const nodes = await coreApi.listNode();
1168
+ const pods = await coreApi.listPodForAllNamespaces();
1169
+ return (nodes.items ?? [])
1170
+ .filter((n) => this.isReadyWorker(n))
1171
+ .map((node) => {
1172
+ const nodeName = node.metadata?.name ?? '';
1173
+ const alloc = node.status?.allocatable ?? {};
1174
+ const allocatable = {
1175
+ cpu: this.parseCpu(alloc['cpu'] ?? '0'),
1176
+ memory: this.parseMemory(alloc['memory'] ?? '0'),
1177
+ };
1178
+ const requested = this.sumPodRequestsOnNode(pods.items ?? [], nodeName);
1179
+ return {
1180
+ nodeName,
1181
+ allocatable,
1182
+ requested,
1183
+ free: {
1184
+ cpu: allocatable.cpu - requested.cpu,
1185
+ memory: allocatable.memory - requested.memory,
1186
+ },
1187
+ };
1188
+ });
1189
+ }
1190
+ isReadyWorker(node) {
1191
+ const labels = node.metadata?.labels ?? {};
1192
+ if ('node-role.kubernetes.io/control-plane' in labels ||
1193
+ 'node-role.kubernetes.io/master' in labels) {
1194
+ return false;
1195
+ }
1196
+ if (!node.metadata?.name)
1197
+ return false;
1198
+ const ready = (node.status?.conditions ?? []).find((c) => c.type === 'Ready');
1199
+ return ready?.status === 'True';
1200
+ }
1201
+ sumPodRequestsOnNode(pods, nodeName) {
1202
+ let cpu = 0;
1203
+ let memory = 0;
1204
+ for (const pod of pods) {
1205
+ if (pod.spec?.nodeName !== nodeName)
1206
+ continue;
1207
+ const phase = pod.status?.phase;
1208
+ if (phase !== 'Running' && phase !== 'Pending')
1209
+ continue;
1210
+ for (const container of pod.spec?.containers ?? []) {
1211
+ const requests = container.resources?.requests ?? {};
1212
+ cpu += this.parseCpu(requests['cpu'] ?? '0');
1213
+ memory += this.parseMemory(requests['memory'] ?? '0');
1214
+ }
1215
+ }
1216
+ return { cpu, memory };
1217
+ }
1121
1218
  /**
1122
1219
  * Parse a Kubernetes CPU string to millicores.
1123
1220
  * Examples: "250m" → 250, "2" → 2000, "0.5" → 500
@@ -82,4 +82,6 @@ export interface ProviderCapabilities {
82
82
  };
83
83
  /** VNet topology info — null when privateNetworking is false */
84
84
  vnetTopology: VNetTopology | null;
85
+ vnetRequired: boolean;
86
+ crossClusterAllowed: boolean;
85
87
  }
@@ -232,6 +232,8 @@ let ContaboCapabilitiesService = ContaboCapabilitiesService_1 = class ContaboCap
232
232
  minimumCost: 4.5, // Cloud VPS 10: 4 vCPU, 8 GB RAM @ €4.50/mo
233
233
  },
234
234
  vnetTopology: null, // Contabo VNet not yet implemented
235
+ vnetRequired: true,
236
+ crossClusterAllowed: false,
235
237
  };
236
238
  }
237
239
  async getCapabilities() {
@@ -127,12 +127,7 @@ let HetznerCapabilitiesService = HetznerCapabilitiesService_1 = class HetznerCap
127
127
  ],
128
128
  },
129
129
  dnsZoneDelegation: {
130
- nameservers: [
131
- 'ns1.your-server.de',
132
- 'ns.second-ns.com',
133
- 'ns3.second-ns.de',
134
- ],
135
- delegationGuideUrl: 'https://docs.hetzner.com/networking/dns/getting-started/delegate-to-hetzner/',
130
+ delegationGuideUrl: 'https://docs.hetzner.com/dns-console/dns/general/delegating-a-domain-to-hetzner-dns/',
136
131
  },
137
132
  };
138
133
  }
@@ -219,6 +214,8 @@ let HetznerCapabilitiesService = HetznerCapabilitiesService_1 = class HetznerCap
219
214
  vnetIpRange: { minPrefix: 8, maxPrefix: 29 },
220
215
  subnetIpRange: { minPrefix: 8, maxPrefix: 29 },
221
216
  },
217
+ vnetRequired: true,
218
+ crossClusterAllowed: false,
222
219
  };
223
220
  }
224
221
  mapHetznerLocationsToRegions(locations) {
@@ -135,7 +135,6 @@ let ScalewayCapabilitiesService = ScalewayCapabilitiesService_1 = class Scaleway
135
135
  ],
136
136
  },
137
137
  dnsZoneDelegation: {
138
- nameservers: ['ns0.dom.scw.cloud', 'ns1.dom.scw.cloud'],
139
138
  delegationGuideUrl: 'https://www.scaleway.com/en/docs/network/domains-and-dns/how-to/add-external-domain/',
140
139
  },
141
140
  };
@@ -226,6 +225,8 @@ let ScalewayCapabilitiesService = ScalewayCapabilitiesService_1 = class Scaleway
226
225
  vnetIpRange: { minPrefix: 20, maxPrefix: 28 },
227
226
  subnetIpRange: { minPrefix: 20, maxPrefix: 28 },
228
227
  },
228
+ vnetRequired: true,
229
+ crossClusterAllowed: false,
229
230
  };
230
231
  }
231
232
  async getCapabilities() {
@@ -401,7 +401,9 @@ let ScalewayFirewallService = ScalewayFirewallService_1 = class ScalewayFirewall
401
401
  this.logger.warn(`Security group ${firewallId} not found, already deleted`);
402
402
  return;
403
403
  }
404
- throw new Error(`Failed to delete security group: ${error.message}`);
404
+ const data = error?.response?.data;
405
+ const detail = data?.help_message || data?.message || error.message;
406
+ throw new Error(`Failed to delete security group: ${detail}`);
405
407
  }
406
408
  }
407
409
  async applyToServers(firewallId, serverIds) {
@@ -1105,7 +1105,9 @@ let ScalewayProviderService = ScalewayProviderService_1 = class ScalewayProvider
1105
1105
  for (const [name, entry] of typeMap.entries()) {
1106
1106
  const t = entry.type;
1107
1107
  const ramBytes = t.ram || 0;
1108
- const ramGb = Math.round(ramBytes / 1e9); // Scaleway uses decimal bytes (1 GB = 1_000_000_000)
1108
+ // RAM is reported in binary bytes (8 GiB = 8_589_934_592), unlike disk
1109
+ // which is decimal. Divide by 1024³ so a "8G" plan reads 8 GB, not 9.
1110
+ const ramGb = Math.round(ramBytes / 1024 ** 3);
1109
1111
  const diskGb = mapDiskGb(t);
1110
1112
  const prices = [];
1111
1113
  for (const [region, p] of entry.pricesByRegion.entries()) {
@@ -64,8 +64,6 @@ export interface ProviderCredentialFields {
64
64
  supportsExpiry: boolean;
65
65
  }
66
66
  export interface DnsZoneDelegation {
67
- /** Nameservers to set at the domain registrar to delegate to this provider */
68
- nameservers: string[];
69
67
  /** Official guide URL explaining how to delegate/import an external domain */
70
68
  delegationGuideUrl: string;
71
69
  }
@@ -44,5 +44,5 @@ export declare class HetznerFirewallService implements IFirewallProvider {
44
44
  getServerIdsByLabelSelector(labelSelector: string): Promise<string[]>;
45
45
  applyToServers(firewallId: string, serverIds: string[]): Promise<void>;
46
46
  removeFromServers(firewallId: string, serverIds: string[]): Promise<void>;
47
- updateFirewallLabels(firewallId: string, labels: Record<string, string>): Promise<void>;
47
+ updateFirewallLabels(firewallId: string, labels: Record<string, string>, name?: string): Promise<void>;
48
48
  }
@@ -445,12 +445,13 @@ let HetznerFirewallService = HetznerFirewallService_1 = class HetznerFirewallSer
445
445
  throw new Error(`Failed to remove firewall: ${error.message}`);
446
446
  }
447
447
  }
448
- async updateFirewallLabels(firewallId, labels) {
448
+ async updateFirewallLabels(firewallId, labels, name) {
449
449
  this.logger.log(`Updating firewall ${firewallId} labels`);
450
450
  try {
451
451
  const firewallsApi = await this.createFirewallsApi();
452
452
  await firewallsApi.updateFirewall(Number.parseInt(firewallId), {
453
453
  labels,
454
+ ...(name ? { name } : {}),
454
455
  });
455
456
  this.logger.log(`Firewall ${firewallId} labels updated successfully: ${JSON.stringify(labels)}`);
456
457
  }