@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.
- 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 +78 -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 +50 -47
- 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 +100 -0
- package/lib/cli/src/config/bootstrap.config.d.ts +10 -1
- package/lib/cli/src/config/bootstrap.config.js +24 -4
- package/lib/cli/src/config/preferences-schema.js +5 -5
- package/lib/cli/src/config/release-override.d.ts +43 -0
- package/lib/cli/src/config/release-override.js +203 -0
- 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 +545 -0
- package/lib/cli/src/services/cli-endpoint-resolver.service.d.ts +1 -0
- package/lib/cli/src/services/cli-endpoint-resolver.service.js +25 -11
- 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 +1201 -854
- package/package.json +2 -2
|
@@ -50,10 +50,11 @@ const path = __importStar(require("node:path"));
|
|
|
50
50
|
const os = __importStar(require("node:os"));
|
|
51
51
|
const cli_logger_service_1 = require("./cli-logger.service");
|
|
52
52
|
const bootstrap_config_1 = require("../config/bootstrap.config");
|
|
53
|
+
const release_override_1 = require("../config/release-override");
|
|
53
54
|
/**
|
|
54
55
|
* CLI K3s Script Service
|
|
55
56
|
*
|
|
56
|
-
* Generates K3s initialization scripts for
|
|
57
|
+
* Generates K3s initialization scripts for control clusters.
|
|
57
58
|
* Uses scripts from cli/src/modules/instances/assets/scripts/ directory.
|
|
58
59
|
*/
|
|
59
60
|
let CliK3sScriptService = CliK3sScriptService_1 = class CliK3sScriptService {
|
|
@@ -93,11 +94,13 @@ let CliK3sScriptService = CliK3sScriptService_1 = class CliK3sScriptService {
|
|
|
93
94
|
try {
|
|
94
95
|
this.log(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`, opId);
|
|
95
96
|
this.log(`[BOOTSTRAP MASTER SCRIPT] Cluster: ${config.clusterName}`, opId);
|
|
96
|
-
|
|
97
|
+
const scriptsBaseUrl = (0, bootstrap_config_1.getScriptsBaseUrl)(config.useLatest ?? false);
|
|
98
|
+
const imageTags = (0, release_override_1.resolveEffectiveImageTags)(config.useLatest ?? false);
|
|
99
|
+
this.log(`Scripts URL: ${scriptsBaseUrl}`, opId);
|
|
97
100
|
// Generate bootstrap script that downloads and executes k3s-master-init.sh from GitHub
|
|
98
101
|
const script = this.generateBootstrapScript('master', {
|
|
99
|
-
SCRIPTS_BASE_URL:
|
|
100
|
-
MANIFESTS_BASE_URL:
|
|
102
|
+
SCRIPTS_BASE_URL: scriptsBaseUrl,
|
|
103
|
+
MANIFESTS_BASE_URL: scriptsBaseUrl.replace('/scripts', '/manifests'),
|
|
101
104
|
SERVER_ID: config.serverId || '', // Database node ID for observability
|
|
102
105
|
INSTANCE_ID: config.instanceId,
|
|
103
106
|
INSTANCE_NAME: config.instanceName,
|
|
@@ -106,6 +109,10 @@ let CliK3sScriptService = CliK3sScriptService_1 = class CliK3sScriptService {
|
|
|
106
109
|
CLUSTER_NAME: config.clusterName,
|
|
107
110
|
K3S_TOKEN: config.k3sToken,
|
|
108
111
|
K3S_VERSION: config.k3sVersion || 'v1.35.4+k3s1',
|
|
112
|
+
// Pinned Flui image tags — consumed by the system manifests via envsubst.
|
|
113
|
+
FLUI_API_IMAGE_TAG: imageTags.fluiApi,
|
|
114
|
+
FLUI_WEB_IMAGE_TAG: imageTags.fluiWeb,
|
|
115
|
+
FLUI_AUTHZ_IMAGE_TAG: imageTags.fluiAuthz,
|
|
109
116
|
DEPLOY_OBSERVABILITY_STACK: config.deployObservabilityStack
|
|
110
117
|
? 'true'
|
|
111
118
|
: 'false',
|
|
@@ -179,11 +186,12 @@ let CliK3sScriptService = CliK3sScriptService_1 = class CliK3sScriptService {
|
|
|
179
186
|
try {
|
|
180
187
|
this.log(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`, opId);
|
|
181
188
|
this.log(`[BOOTSTRAP WORKER SCRIPT] Cluster: ${config.clusterName}`, opId);
|
|
182
|
-
|
|
189
|
+
const scriptsBaseUrl = (0, bootstrap_config_1.getScriptsBaseUrl)(config.useLatest ?? false);
|
|
190
|
+
this.log(`Scripts URL: ${scriptsBaseUrl}`, opId);
|
|
183
191
|
// Generate bootstrap script that downloads and executes k3s-worker-init.sh from GitHub
|
|
184
192
|
const script = this.generateBootstrapScript('worker', {
|
|
185
193
|
SERVER_ID: config.serverId || '', // Database node ID for observability
|
|
186
|
-
SCRIPTS_BASE_URL:
|
|
194
|
+
SCRIPTS_BASE_URL: scriptsBaseUrl,
|
|
187
195
|
INSTANCE_ID: config.instanceId,
|
|
188
196
|
INSTANCE_NAME: config.instanceName,
|
|
189
197
|
CLOUD_PROVIDER: config.provider,
|
|
@@ -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.1',
|
|
15
|
+
bootstrapRef: 'v0.5.1',
|
|
16
|
+
images: {
|
|
17
|
+
fluiApi: '0.5.1',
|
|
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
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
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
|
-
*
|
|
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)
|
|
@@ -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 = {}));
|
|
@@ -11,9 +11,15 @@ export declare enum ClusterStatus {
|
|
|
11
11
|
DELETED = "deleted"
|
|
12
12
|
}
|
|
13
13
|
export declare enum ClusterType {
|
|
14
|
-
|
|
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["
|
|
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(', ')}).
|
|
384
|
-
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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.
|
|
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.
|
|
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
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
622
|
+
const stdoutStream = new node_stream_1.Writable({
|
|
623
|
+
write(chunk, _enc, cb) {
|
|
624
|
+
stdout += chunk.toString();
|
|
625
|
+
cb();
|
|
625
626
|
},
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
|
|
627
|
+
});
|
|
628
|
+
const stderrStream = new node_stream_1.Writable({
|
|
629
|
+
write(chunk, _enc, cb) {
|
|
630
|
+
stderr += chunk.toString();
|
|
631
|
+
cb();
|
|
629
632
|
},
|
|
630
|
-
}
|
|
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
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
47
|
+
updateFirewallLabels(firewallId: string, labels: Record<string, string>, name?: string): Promise<void>;
|
|
48
48
|
}
|