@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
|
@@ -42,7 +42,7 @@ const ora_1 = __importDefault(require("ora"));
|
|
|
42
42
|
const fs = __importStar(require("node:fs"));
|
|
43
43
|
const path = __importStar(require("node:path"));
|
|
44
44
|
const nest_app_1 = require("../../lib/nest-app");
|
|
45
|
-
const
|
|
45
|
+
const cli_control_cluster_service_1 = require("../../services/cli-control-cluster.service");
|
|
46
46
|
const cli_endpoint_resolver_service_1 = require("../../services/cli-endpoint-resolver.service");
|
|
47
47
|
const config_storage_1 = require("../../lib/config-storage");
|
|
48
48
|
const cluster_entity_1 = require("../../../../src/modules/infrastructure/clusters/entities/cluster.entity");
|
|
@@ -59,12 +59,12 @@ class EnvExportConfig extends core_1.Command {
|
|
|
59
59
|
let spinner = (0, ora_1.default)('Reading cluster configuration...').start();
|
|
60
60
|
try {
|
|
61
61
|
const app = await (0, nest_app_1.getNestApp)();
|
|
62
|
-
const
|
|
62
|
+
const controlService = app.get(cli_control_cluster_service_1.CliControlClusterService);
|
|
63
63
|
const resolver = app.get(cli_endpoint_resolver_service_1.CliEndpointResolverService);
|
|
64
|
-
const cluster = await
|
|
64
|
+
const cluster = await controlService.getControlCluster();
|
|
65
65
|
if (!cluster) {
|
|
66
|
-
spinner.fail('No
|
|
67
|
-
console.log(chalk_1.default.yellow('\nâ ď¸ No
|
|
66
|
+
spinner.fail('No control cluster found');
|
|
67
|
+
console.log(chalk_1.default.yellow('\nâ ď¸ No control cluster exists.\n'));
|
|
68
68
|
console.log(chalk_1.default.dim('Create one with:'));
|
|
69
69
|
console.log(` ${chalk_1.default.cyan('flui env create')}\n`);
|
|
70
70
|
return;
|
|
@@ -83,17 +83,12 @@ class EnvExportConfig extends core_1.Command {
|
|
|
83
83
|
spinner = (0, ora_1.default)('Resolving endpoints from cluster...').start();
|
|
84
84
|
const endpoints = await resolver.resolveEndpoints(masterIp, cluster.nipHostnameToken);
|
|
85
85
|
spinner.succeed('Endpoints resolved');
|
|
86
|
-
//
|
|
87
|
-
// them here would require kube-API access (closed by the new firewall
|
|
88
|
-
// policy). They are written to .env.local by `flui dev creds`, which
|
|
89
|
-
// reaches the secret over SSH+kubectl. Here we fall back to the
|
|
90
|
-
// 'email' preference and skip the public CA key.
|
|
86
|
+
// Written to .env.local by `flui dev creds` (reads flui-secrets over SSH+kubectl).
|
|
91
87
|
const adminEmail = '';
|
|
92
88
|
const sshCaPublicKey = '';
|
|
93
89
|
const authMode = endpoints.authMode === 'unknown' ? 'local' : endpoints.authMode;
|
|
94
90
|
const resolvedIssuer = this.resolveIssuer(endpoints);
|
|
95
91
|
const resolvedJwks = this.resolveJwks(endpoints, resolvedIssuer);
|
|
96
|
-
// Display configuration
|
|
97
92
|
console.log(chalk_1.default.cyan('\nđ Exporting Cluster Configuration\n'));
|
|
98
93
|
console.log(` ${chalk_1.default.bold('Cluster:')} ${cluster.name}`);
|
|
99
94
|
console.log(` ${chalk_1.default.bold('Master IP:')} ${masterIp}`);
|
|
@@ -113,7 +108,16 @@ class EnvExportConfig extends core_1.Command {
|
|
|
113
108
|
console.log(` OIDC_ISSUER=${resolvedIssuer || chalk_1.default.dim('(not set)')}`);
|
|
114
109
|
console.log(` OIDC_JWKS_URI=${resolvedJwks || chalk_1.default.dim('(not set)')}`);
|
|
115
110
|
console.log(` OIDC_AUDIENCE=${endpoints.oidcAudience || chalk_1.default.dim('(set after first Zitadel setup)')}`);
|
|
111
|
+
const adminUrlPreview = endpoints.zitadel.fqdn
|
|
112
|
+
? `https://${endpoints.zitadel.fqdn}`
|
|
113
|
+
: chalk_1.default.dim('(zitadel ingress not found)');
|
|
114
|
+
console.log(` OIDC_PROVIDER_ADMIN_URL=${adminUrlPreview}`);
|
|
115
|
+
console.log(` OIDC_CLI_CLIENT_ID=${endpoints.oidcCliClientId || chalk_1.default.dim('(not yet provisioned)')}`);
|
|
116
116
|
}
|
|
117
|
+
const publicWebUrlPreview = endpoints.fluiWeb.fqdn
|
|
118
|
+
? `https://${endpoints.fluiWeb.fqdn}`
|
|
119
|
+
: chalk_1.default.dim('(flui-web ingress not found)');
|
|
120
|
+
console.log(` PUBLIC_WEB_URL=${publicWebUrlPreview}`);
|
|
117
121
|
console.log(` AUTH_MODE=${authMode}`);
|
|
118
122
|
console.log(` ADMIN_EMAIL=${adminEmail || chalk_1.default.dim('(not set)')}`);
|
|
119
123
|
console.log(` SSH_CA_PUBLIC_KEY=${sshCaPublicKey ? chalk_1.default.green('(present)') : chalk_1.default.dim('(not set)')}`);
|
|
@@ -138,8 +142,6 @@ class EnvExportConfig extends core_1.Command {
|
|
|
138
142
|
skipDashboard: flags['no-dashboard'],
|
|
139
143
|
});
|
|
140
144
|
spinner = (0, ora_1.default)('Updating .env file...').start();
|
|
141
|
-
// .env lives next to the resolved apiPath. Defaults to '.' (cwd) so legacy
|
|
142
|
-
// invocations from inside flui.api keep working without any preference set.
|
|
143
145
|
const apiDir = path.isAbsolute(preferences.apiPath)
|
|
144
146
|
? preferences.apiPath
|
|
145
147
|
: path.resolve(process.cwd(), preferences.apiPath);
|
|
@@ -184,8 +186,15 @@ class EnvExportConfig extends core_1.Command {
|
|
|
184
186
|
envVars.OIDC_JWKS_URI = resolvedJwks;
|
|
185
187
|
if (endpoints.oidcAudience)
|
|
186
188
|
envVars.OIDC_AUDIENCE = endpoints.oidcAudience;
|
|
187
|
-
//
|
|
188
|
-
|
|
189
|
+
// Zitadel admin API enforces Host header matching ExternalDomain.
|
|
190
|
+
if (endpoints.zitadel.fqdn)
|
|
191
|
+
envVars.OIDC_PROVIDER_ADMIN_URL = `https://${endpoints.zitadel.fqdn}`;
|
|
192
|
+
// Avoids OidcBootstrapSeeder re-provisioning the CLI app on every boot.
|
|
193
|
+
if (endpoints.oidcCliClientId)
|
|
194
|
+
envVars.OIDC_CLI_CLIENT_ID = endpoints.oidcCliClientId;
|
|
195
|
+
// Consumed by flui-authz install for the login-redirect URL.
|
|
196
|
+
if (endpoints.fluiWeb.fqdn)
|
|
197
|
+
envVars.PUBLIC_WEB_URL = `https://${endpoints.fluiWeb.fqdn}`;
|
|
189
198
|
const resolvedEmail = adminEmail || preferences.email;
|
|
190
199
|
if (resolvedEmail)
|
|
191
200
|
envVars.ADMIN_EMAIL = resolvedEmail;
|
|
@@ -220,15 +229,6 @@ class EnvExportConfig extends core_1.Command {
|
|
|
220
229
|
await (0, nest_app_1.closeNestApp)();
|
|
221
230
|
}
|
|
222
231
|
}
|
|
223
|
-
/**
|
|
224
|
-
* Resolve every preference this command consumes (email, dashboardPath, certificateMode)
|
|
225
|
-
* through the layered config: flag > env > project-local > user > default > prompt.
|
|
226
|
-
*
|
|
227
|
-
* Prompted values are persisted to the active profile only when --save is set;
|
|
228
|
-
* otherwise they are used for this run and forgotten (no surprise side effects).
|
|
229
|
-
*
|
|
230
|
-
* `skipDashboard` shortcuts the dashboard-only preferences when --no-dashboard is set.
|
|
231
|
-
*/
|
|
232
232
|
async resolvePreferences(opts) {
|
|
233
233
|
const storage = new config_storage_1.ConfigStorage();
|
|
234
234
|
const resolver = new preferences_resolver_1.PreferencesResolver(storage);
|
|
@@ -264,10 +264,6 @@ class EnvExportConfig extends core_1.Command {
|
|
|
264
264
|
}
|
|
265
265
|
return out;
|
|
266
266
|
}
|
|
267
|
-
/**
|
|
268
|
-
* Patch the dashboard's config.json with the localhost API endpoints + cluster auth +
|
|
269
|
-
* the resolved certificateMode. Caller is responsible for resolving inputs.
|
|
270
|
-
*/
|
|
271
267
|
async syncDashboardConfig(opts) {
|
|
272
268
|
const dashboardPath = opts.dashboardPath;
|
|
273
269
|
const certificateMode = opts.certificateMode;
|
|
@@ -275,20 +271,31 @@ class EnvExportConfig extends core_1.Command {
|
|
|
275
271
|
? dashboardPath
|
|
276
272
|
: path.resolve(process.cwd(), dashboardPath);
|
|
277
273
|
const configPath = path.join(absoluteDashboardPath, 'src', 'assets', 'config.json');
|
|
274
|
+
const examplePath = path.join(absoluteDashboardPath, 'src', 'assets', 'config.example.json');
|
|
278
275
|
const spinner = (0, ora_1.default)(`Updating dashboard config (${configPath})...`).start();
|
|
279
|
-
if (!fs.existsSync(configPath)) {
|
|
280
|
-
spinner.warn(`Dashboard config not found at ${configPath} â skipping. Adjust dashboardPath or pass --no-dashboard.`);
|
|
281
|
-
return;
|
|
282
|
-
}
|
|
283
276
|
let current = {};
|
|
284
|
-
|
|
285
|
-
|
|
277
|
+
if (fs.existsSync(configPath)) {
|
|
278
|
+
try {
|
|
279
|
+
current = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
280
|
+
}
|
|
281
|
+
catch (err) {
|
|
282
|
+
spinner.fail(`Dashboard config.json is not valid JSON (${err.message}) â skipping`);
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
else if (fs.existsSync(examplePath)) {
|
|
287
|
+
try {
|
|
288
|
+
current = JSON.parse(fs.readFileSync(examplePath, 'utf-8'));
|
|
289
|
+
spinner.info(`config.json missing â seeded from ${path.basename(examplePath)}`);
|
|
290
|
+
}
|
|
291
|
+
catch (err) {
|
|
292
|
+
spinner.warn(`config.example.json is not valid JSON (${err.message}) â starting from empty config`);
|
|
293
|
+
}
|
|
286
294
|
}
|
|
287
|
-
|
|
288
|
-
spinner.
|
|
289
|
-
return;
|
|
295
|
+
else {
|
|
296
|
+
spinner.info('config.json and config.example.json both missing â creating from scratch');
|
|
290
297
|
}
|
|
291
|
-
if (opts.backup) {
|
|
298
|
+
if (opts.backup && fs.existsSync(configPath)) {
|
|
292
299
|
const timestamp = new Date()
|
|
293
300
|
.toISOString()
|
|
294
301
|
.replaceAll(':', '-')
|
|
@@ -300,18 +307,16 @@ class EnvExportConfig extends core_1.Command {
|
|
|
300
307
|
apiBaseUrl: 'http://localhost:3000',
|
|
301
308
|
wsUrl: 'ws://localhost:3000',
|
|
302
309
|
authMode: opts.authMode,
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
310
|
+
// Always reflect the resolved cluster â never keep a previous cluster's
|
|
311
|
+
// value. Empty oidcClientId is fine: the dashboard fetches it at runtime.
|
|
312
|
+
oidcIssuer: opts.oidcIssuer || '',
|
|
313
|
+
oidcClientId: opts.oidcClientId || '',
|
|
314
|
+
oidcAudience: opts.oidcAudience || '',
|
|
306
315
|
certificateMode,
|
|
307
316
|
};
|
|
308
317
|
fs.writeFileSync(configPath, JSON.stringify(updated, null, 2) + '\n', 'utf-8');
|
|
309
318
|
spinner.succeed(`Dashboard config updated (authMode=${opts.authMode}, certificateMode=${certificateMode}, oidcClientId=${opts.oidcClientId ? 'set' : '(empty)'})`);
|
|
310
319
|
}
|
|
311
|
-
/**
|
|
312
|
-
* Prompt for a missing preference value, validating against the schema.
|
|
313
|
-
* Used only when every other layer (flag, env, project, user, default) failed to provide a value.
|
|
314
|
-
*/
|
|
315
320
|
async promptForPreference(key) {
|
|
316
321
|
const def = preferences_schema_1.PREFERENCES[key];
|
|
317
322
|
return (0, prompts_1.promptInput)({
|
|
@@ -363,8 +368,6 @@ EnvExportConfig.flags = {
|
|
|
363
368
|
description: 'Skip updating the dashboard config.json',
|
|
364
369
|
default: false,
|
|
365
370
|
}),
|
|
366
|
-
// Direct overrides for the layered preferences resolver.
|
|
367
|
-
// Without these, values resolve from env > project file > user config > prompt.
|
|
368
371
|
'api-path': core_1.Flags.string({
|
|
369
372
|
description: `Override resolved value for the "apiPath" preference (${preferences_schema_1.PREFERENCES.apiPath.description})`,
|
|
370
373
|
}),
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Command } from '@oclif/core';
|
|
2
2
|
export default class EnvForceReady extends Command {
|
|
3
|
-
static readonly description = "Force
|
|
3
|
+
static readonly description = "Force control cluster status to READY (use when cluster is working but stuck in ERROR or CREATING state)";
|
|
4
4
|
static readonly examples: string[];
|
|
5
5
|
static readonly flags: {
|
|
6
6
|
force: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
@@ -41,7 +41,7 @@ const chalk_1 = __importDefault(require("chalk"));
|
|
|
41
41
|
const ora_1 = __importDefault(require("ora"));
|
|
42
42
|
const nest_app_1 = require("../../lib/nest-app");
|
|
43
43
|
const nip_base_domain_util_1 = require("../../lib/nip-base-domain.util");
|
|
44
|
-
const
|
|
44
|
+
const cli_control_cluster_service_1 = require("../../services/cli-control-cluster.service");
|
|
45
45
|
const cli_cluster_repository_1 = require("../../lib/repositories/cli-cluster.repository");
|
|
46
46
|
const cli_operation_repository_1 = require("../../lib/repositories/cli-operation.repository");
|
|
47
47
|
const cluster_entity_1 = require("../../../../src/modules/infrastructure/clusters/entities/cluster.entity");
|
|
@@ -59,14 +59,14 @@ class EnvForceReady extends core_1.Command {
|
|
|
59
59
|
spinner.stop();
|
|
60
60
|
console.log(chalk_1.default.yellow('\nâ ď¸ Force Cluster Status to READY\n'));
|
|
61
61
|
spinner = (0, ora_1.default)('Loading cluster...').start();
|
|
62
|
-
const
|
|
62
|
+
const controlService = app.get(cli_control_cluster_service_1.CliControlClusterService);
|
|
63
63
|
const clusterRepository = app.get(cli_cluster_repository_1.CliClusterRepository);
|
|
64
64
|
const operationRepository = app.get(cli_operation_repository_1.CliOperationRepository);
|
|
65
|
-
// Find
|
|
66
|
-
const cluster = await
|
|
65
|
+
// Find control cluster
|
|
66
|
+
const cluster = await controlService.getControlCluster();
|
|
67
67
|
if (!cluster) {
|
|
68
|
-
spinner.fail('No
|
|
69
|
-
console.log(chalk_1.default.yellow('\nâ ď¸ No
|
|
68
|
+
spinner.fail('No control cluster found');
|
|
69
|
+
console.log(chalk_1.default.yellow('\nâ ď¸ No control cluster exists.\n'));
|
|
70
70
|
console.log(chalk_1.default.dim('Create one with:'));
|
|
71
71
|
console.log(` ${chalk_1.default.cyan('flui env create')}\n`);
|
|
72
72
|
return;
|
|
@@ -96,7 +96,7 @@ class EnvForceReady extends core_1.Command {
|
|
|
96
96
|
console.log(chalk_1.default.cyan('\nđ Checking cluster health...\n'));
|
|
97
97
|
spinner = (0, ora_1.default)('Checking observability services...').start();
|
|
98
98
|
try {
|
|
99
|
-
healthStatus = await
|
|
99
|
+
healthStatus = await controlService.checkObservabilityServices(cluster.masterIpAddress, cluster.nipHostnameToken);
|
|
100
100
|
spinner.succeed('Health checks completed');
|
|
101
101
|
// Display health status
|
|
102
102
|
console.log(chalk_1.default.cyan('\nđ Service Health Status:\n'));
|
|
@@ -202,7 +202,7 @@ class EnvForceReady extends core_1.Command {
|
|
|
202
202
|
}
|
|
203
203
|
}
|
|
204
204
|
}
|
|
205
|
-
EnvForceReady.description = 'Force
|
|
205
|
+
EnvForceReady.description = 'Force control cluster status to READY (use when cluster is working but stuck in ERROR or CREATING state)';
|
|
206
206
|
EnvForceReady.examples = [
|
|
207
207
|
'<%= config.bin %> <%= command.id %>',
|
|
208
208
|
'<%= config.bin %> <%= command.id %> --force',
|
|
@@ -7,7 +7,7 @@ const core_1 = require("@oclif/core");
|
|
|
7
7
|
const chalk_1 = __importDefault(require("chalk"));
|
|
8
8
|
const ora_1 = __importDefault(require("ora"));
|
|
9
9
|
const nest_app_1 = require("../../lib/nest-app");
|
|
10
|
-
const
|
|
10
|
+
const cli_control_cluster_service_1 = require("../../services/cli-control-cluster.service");
|
|
11
11
|
const cli_ssh_service_1 = require("../../services/cli-ssh.service");
|
|
12
12
|
const context_banner_1 = require("../../lib/context-banner");
|
|
13
13
|
class EnvInspect extends core_1.Command {
|
|
@@ -17,13 +17,13 @@ class EnvInspect extends core_1.Command {
|
|
|
17
17
|
let spinner = (0, ora_1.default)('Loading cluster information...').start();
|
|
18
18
|
try {
|
|
19
19
|
const app = await (0, nest_app_1.getNestApp)();
|
|
20
|
-
const
|
|
20
|
+
const controlService = app.get(cli_control_cluster_service_1.CliControlClusterService);
|
|
21
21
|
const sshService = app.get(cli_ssh_service_1.CliSshService);
|
|
22
22
|
// Get cluster
|
|
23
|
-
const cluster = await
|
|
23
|
+
const cluster = await controlService.getControlCluster();
|
|
24
24
|
if (!cluster) {
|
|
25
|
-
spinner.fail('No
|
|
26
|
-
console.log(chalk_1.default.yellow('\nâ ď¸ No
|
|
25
|
+
spinner.fail('No control cluster found');
|
|
26
|
+
console.log(chalk_1.default.yellow('\nâ ď¸ No control cluster exists.\n'));
|
|
27
27
|
console.log(chalk_1.default.dim('Create one with:'));
|
|
28
28
|
console.log(` ${chalk_1.default.cyan('flui env create')}\n`);
|
|
29
29
|
return;
|
|
@@ -7,7 +7,7 @@ const core_1 = require("@oclif/core");
|
|
|
7
7
|
const chalk_1 = __importDefault(require("chalk"));
|
|
8
8
|
const ora_1 = __importDefault(require("ora"));
|
|
9
9
|
const nest_app_1 = require("../../lib/nest-app");
|
|
10
|
-
const
|
|
10
|
+
const cli_control_cluster_service_1 = require("../../services/cli-control-cluster.service");
|
|
11
11
|
const cli_ssh_service_1 = require("../../services/cli-ssh.service");
|
|
12
12
|
const cli_cluster_repository_1 = require("../../lib/repositories/cli-cluster.repository");
|
|
13
13
|
const encryption_service_1 = require("../../../../src/modules/shared/encryption/services/encryption.service");
|
|
@@ -19,13 +19,13 @@ class EnvRefreshKubeconfig extends core_1.Command {
|
|
|
19
19
|
const spinner = (0, ora_1.default)('Fetching kubeconfig from K3s master...').start();
|
|
20
20
|
try {
|
|
21
21
|
const app = await (0, nest_app_1.getNestApp)();
|
|
22
|
-
const
|
|
22
|
+
const controlService = app.get(cli_control_cluster_service_1.CliControlClusterService);
|
|
23
23
|
const sshService = app.get(cli_ssh_service_1.CliSshService);
|
|
24
24
|
const clusterRepo = app.get(cli_cluster_repository_1.CliClusterRepository);
|
|
25
25
|
const encryptionService = app.get(encryption_service_1.EncryptionService);
|
|
26
|
-
const cluster = await
|
|
26
|
+
const cluster = await controlService.getControlCluster();
|
|
27
27
|
if (!cluster) {
|
|
28
|
-
spinner.fail('No
|
|
28
|
+
spinner.fail('No control cluster found');
|
|
29
29
|
return;
|
|
30
30
|
}
|
|
31
31
|
if (cluster.status !== cluster_entity_1.ClusterStatus.READY) {
|
|
@@ -43,7 +43,7 @@ const fs = __importStar(require("node:fs/promises"));
|
|
|
43
43
|
const path = __importStar(require("node:path"));
|
|
44
44
|
const os = __importStar(require("node:os"));
|
|
45
45
|
const nest_app_1 = require("../../lib/nest-app");
|
|
46
|
-
const
|
|
46
|
+
const cli_control_cluster_service_1 = require("../../services/cli-control-cluster.service");
|
|
47
47
|
const cli_ssh_service_1 = require("../../services/cli-ssh.service");
|
|
48
48
|
const context_banner_1 = require("../../lib/context-banner");
|
|
49
49
|
class EnvRepairSshCa extends core_1.Command {
|
|
@@ -71,11 +71,11 @@ class EnvRepairSshCa extends core_1.Command {
|
|
|
71
71
|
}
|
|
72
72
|
const app = await (0, nest_app_1.getNestApp)();
|
|
73
73
|
try {
|
|
74
|
-
const obs = app.get(
|
|
74
|
+
const obs = app.get(cli_control_cluster_service_1.CliControlClusterService);
|
|
75
75
|
const ssh = app.get(cli_ssh_service_1.CliSshService);
|
|
76
|
-
const cluster = await obs.
|
|
76
|
+
const cluster = await obs.getControlCluster();
|
|
77
77
|
if (!cluster?.masterIpAddress) {
|
|
78
|
-
this.log(chalk_1.default.red(' No
|
|
78
|
+
this.log(chalk_1.default.red(' No control cluster found in this profile.'));
|
|
79
79
|
this.exit(1);
|
|
80
80
|
return;
|
|
81
81
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
export default class EnvRepairStorage extends Command {
|
|
3
|
+
static readonly description = "Backfill the shared-storage volume id into the cluster DB. Repairs clusters whose flui-secrets/DB never received FLUI_SHARED_STORAGE_VOLUME_ID at create \u2014 symptom is sharedStorageVolumeId NULL in the DB and \"no Flui-managed shared storage volume\" on storage expand. Reads the volume id from the active profile, patches flui-secrets over SSH, then restarts flui-api so the bootstrap seeder backfills the DB.";
|
|
4
|
+
static readonly examples: string[];
|
|
5
|
+
static readonly flags: {
|
|
6
|
+
'no-restart': import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
7
|
+
};
|
|
8
|
+
run(): Promise<void>;
|
|
9
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const core_1 = require("@oclif/core");
|
|
7
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
8
|
+
const ora_1 = __importDefault(require("ora"));
|
|
9
|
+
const nest_app_1 = require("../../lib/nest-app");
|
|
10
|
+
const cli_control_cluster_service_1 = require("../../services/cli-control-cluster.service");
|
|
11
|
+
const cli_ssh_service_1 = require("../../services/cli-ssh.service");
|
|
12
|
+
const context_banner_1 = require("../../lib/context-banner");
|
|
13
|
+
class EnvRepairStorage extends core_1.Command {
|
|
14
|
+
async run() {
|
|
15
|
+
const { flags } = await this.parse(EnvRepairStorage);
|
|
16
|
+
(0, context_banner_1.printContextBanner)();
|
|
17
|
+
const app = await (0, nest_app_1.getNestApp)();
|
|
18
|
+
try {
|
|
19
|
+
const control = app.get(cli_control_cluster_service_1.CliControlClusterService);
|
|
20
|
+
const ssh = app.get(cli_ssh_service_1.CliSshService);
|
|
21
|
+
const cluster = await control.getControlCluster();
|
|
22
|
+
if (!cluster?.masterIpAddress) {
|
|
23
|
+
this.log(chalk_1.default.red(' No control cluster found in this profile.'));
|
|
24
|
+
this.exit(1);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
const volumeId = cluster.sharedStorageVolumeId;
|
|
28
|
+
const sizeGb = cluster.sharedStorageVolumeSizeGb;
|
|
29
|
+
if (!volumeId) {
|
|
30
|
+
this.log(chalk_1.default.red(' No shared-storage volume id in this profile â nothing to backfill.'));
|
|
31
|
+
this.exit(1);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const b64 = (s) => Buffer.from(s).toString('base64');
|
|
35
|
+
const ops = [
|
|
36
|
+
`{"op":"add","path":"/data/FLUI_SHARED_STORAGE_VOLUME_ID","value":"${b64(volumeId)}"}`,
|
|
37
|
+
];
|
|
38
|
+
if (sizeGb) {
|
|
39
|
+
ops.push(`{"op":"add","path":"/data/FLUI_SHARED_STORAGE_VOLUME_GB","value":"${b64(String(sizeGb))}"}`);
|
|
40
|
+
}
|
|
41
|
+
const patchSpinner = (0, ora_1.default)(`Patching flui-secrets on ${cluster.masterIpAddress}...`).start();
|
|
42
|
+
const patchCmd = `kubectl -n flui-system patch secret flui-secrets ` +
|
|
43
|
+
`--type='json' -p='[${ops.join(',')}]'`;
|
|
44
|
+
try {
|
|
45
|
+
await ssh.sshExec(cluster.masterIpAddress, patchCmd);
|
|
46
|
+
patchSpinner.succeed(`Secret patched (FLUI_SHARED_STORAGE_VOLUME_ID=${volumeId})`);
|
|
47
|
+
}
|
|
48
|
+
catch (err) {
|
|
49
|
+
patchSpinner.fail(`Patch failed: ${err.message}`);
|
|
50
|
+
this.exit(1);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
if (!flags['no-restart']) {
|
|
54
|
+
const restartSpinner = (0, ora_1.default)('Restarting flui-api...').start();
|
|
55
|
+
try {
|
|
56
|
+
await ssh.sshExec(cluster.masterIpAddress, 'kubectl -n flui-system rollout restart deployment/flui-api');
|
|
57
|
+
restartSpinner.succeed('flui-api rolling restart triggered');
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
restartSpinner.warn(`Restart failed (Secret was patched OK): ${err.message}`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
this.log('');
|
|
64
|
+
this.log(chalk_1.default.green(' â
Storage repair complete. On flui-api boot the seeder backfills sharedStorageVolumeId in the DB.'));
|
|
65
|
+
this.log(chalk_1.default.dim(' Verify with `flui env storage` or the storage-expand endpoint once flui-api is back up.\n'));
|
|
66
|
+
}
|
|
67
|
+
finally {
|
|
68
|
+
await (0, nest_app_1.closeNestApp)();
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
EnvRepairStorage.description = 'Backfill the shared-storage volume id into the cluster DB. Repairs clusters whose flui-secrets/DB never received FLUI_SHARED_STORAGE_VOLUME_ID at create â symptom is sharedStorageVolumeId NULL in the DB and "no Flui-managed shared storage volume" on storage expand. Reads the volume id from the active profile, patches flui-secrets over SSH, then restarts flui-api so the bootstrap seeder backfills the DB.';
|
|
73
|
+
EnvRepairStorage.examples = [
|
|
74
|
+
'<%= config.bin %> <%= command.id %>',
|
|
75
|
+
'<%= config.bin %> <%= command.id %> --no-restart',
|
|
76
|
+
];
|
|
77
|
+
EnvRepairStorage.flags = {
|
|
78
|
+
'no-restart': core_1.Flags.boolean({
|
|
79
|
+
description: 'Skip the flui-api rolling restart after patching the Secret',
|
|
80
|
+
}),
|
|
81
|
+
};
|
|
82
|
+
exports.default = EnvRepairStorage;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Command } from '@oclif/core';
|
|
2
2
|
export default class EnvRestart extends Command {
|
|
3
|
-
static readonly description = "Restart stopped
|
|
3
|
+
static readonly description = "Restart stopped control cluster servers";
|
|
4
4
|
static readonly examples: string[];
|
|
5
5
|
run(): Promise<void>;
|
|
6
6
|
/**
|
|
@@ -7,7 +7,7 @@ const core_1 = require("@oclif/core");
|
|
|
7
7
|
const chalk_1 = __importDefault(require("chalk"));
|
|
8
8
|
const ora_1 = __importDefault(require("ora"));
|
|
9
9
|
const nest_app_1 = require("../../lib/nest-app");
|
|
10
|
-
const
|
|
10
|
+
const cli_control_cluster_service_1 = require("../../services/cli-control-cluster.service");
|
|
11
11
|
const hetzner_provider_service_1 = require("../../../../src/modules/providers/services/hetzner-provider.service");
|
|
12
12
|
const cluster_entity_1 = require("../../../../src/modules/infrastructure/clusters/entities/cluster.entity");
|
|
13
13
|
const cli_cluster_repository_1 = require("../../lib/repositories/cli-cluster.repository");
|
|
@@ -20,14 +20,14 @@ class EnvRestart extends core_1.Command {
|
|
|
20
20
|
// Bootstrap NestJS and get services
|
|
21
21
|
const app = await (0, nest_app_1.getNestApp)();
|
|
22
22
|
spinner.succeed('Initialized');
|
|
23
|
-
console.log(chalk_1.default.cyan('\nđ Restarting
|
|
23
|
+
console.log(chalk_1.default.cyan('\nđ Restarting Control Cluster\n'));
|
|
24
24
|
spinner = (0, ora_1.default)('Finding cluster...').start();
|
|
25
|
-
const
|
|
26
|
-
// Get
|
|
27
|
-
const cluster = await
|
|
25
|
+
const controlService = app.get(cli_control_cluster_service_1.CliControlClusterService);
|
|
26
|
+
// Get control cluster
|
|
27
|
+
const cluster = await controlService.getControlCluster();
|
|
28
28
|
if (!cluster) {
|
|
29
|
-
spinner.fail('No
|
|
30
|
-
console.log(chalk_1.default.yellow('\nâ ď¸ No
|
|
29
|
+
spinner.fail('No control cluster found');
|
|
30
|
+
console.log(chalk_1.default.yellow('\nâ ď¸ No control cluster exists.\n'));
|
|
31
31
|
console.log(`Create one with: ${chalk_1.default.cyan('flui env create')}\n`);
|
|
32
32
|
return;
|
|
33
33
|
}
|
|
@@ -117,7 +117,7 @@ class EnvRestart extends core_1.Command {
|
|
|
117
117
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
118
118
|
spinner = (0, ora_1.default)(`Checking services (attempt ${attempt}/${maxAttempts})...`).start();
|
|
119
119
|
try {
|
|
120
|
-
const servicesHealth = await
|
|
120
|
+
const servicesHealth = await controlService.checkObservabilityServices(cluster.masterIpAddress, cluster.nipHostnameToken);
|
|
121
121
|
const allHealthy = servicesHealth.prometheus === 'healthy' &&
|
|
122
122
|
servicesHealth.grafana === 'healthy' &&
|
|
123
123
|
servicesHealth.loki === 'healthy';
|
|
@@ -182,6 +182,6 @@ class EnvRestart extends core_1.Command {
|
|
|
182
182
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
183
183
|
}
|
|
184
184
|
}
|
|
185
|
-
EnvRestart.description = 'Restart stopped
|
|
185
|
+
EnvRestart.description = 'Restart stopped control cluster servers';
|
|
186
186
|
EnvRestart.examples = ['<%= config.bin %> <%= command.id %>'];
|
|
187
187
|
exports.default = EnvRestart;
|
|
@@ -8,7 +8,7 @@ const chalk_1 = __importDefault(require("chalk"));
|
|
|
8
8
|
const ora_1 = __importDefault(require("ora"));
|
|
9
9
|
const nest_app_1 = require("../../lib/nest-app");
|
|
10
10
|
const context_banner_1 = require("../../lib/context-banner");
|
|
11
|
-
const
|
|
11
|
+
const cli_control_cluster_service_1 = require("../../services/cli-control-cluster.service");
|
|
12
12
|
const cluster_node_scaling_service_1 = require("../../../../src/modules/infrastructure/clusters/services/cluster-node-scaling.service");
|
|
13
13
|
const cluster_capacity_service_1 = require("../../../../src/modules/infrastructure/clusters/services/cluster-capacity.service");
|
|
14
14
|
const cli_node_repository_1 = require("../../lib/repositories/cli-node.repository");
|
|
@@ -21,13 +21,13 @@ class EnvScaleMaster extends core_1.Command {
|
|
|
21
21
|
const spinner = (0, ora_1.default)('Preparing scale-master plan...').start();
|
|
22
22
|
try {
|
|
23
23
|
const app = await (0, nest_app_1.getNestApp)();
|
|
24
|
-
const
|
|
24
|
+
const controlService = app.get(cli_control_cluster_service_1.CliControlClusterService);
|
|
25
25
|
const nodeRepo = app.get(cli_node_repository_1.CliNodeRepository);
|
|
26
26
|
const capacityService = app.get(cluster_capacity_service_1.ClusterCapacityService);
|
|
27
27
|
const scalingService = app.get(cluster_node_scaling_service_1.ClusterNodeScalingService);
|
|
28
|
-
const cluster = await
|
|
28
|
+
const cluster = await controlService.getControlCluster();
|
|
29
29
|
if (!cluster) {
|
|
30
|
-
spinner.fail('No
|
|
30
|
+
spinner.fail('No control cluster found');
|
|
31
31
|
return;
|
|
32
32
|
}
|
|
33
33
|
const nodes = await nodeRepo.find({ where: { clusterId: cluster.id } });
|
|
@@ -8,7 +8,7 @@ const chalk_1 = __importDefault(require("chalk"));
|
|
|
8
8
|
const ora_1 = __importDefault(require("ora"));
|
|
9
9
|
const nest_app_1 = require("../../lib/nest-app");
|
|
10
10
|
const context_banner_1 = require("../../lib/context-banner");
|
|
11
|
-
const
|
|
11
|
+
const cli_control_cluster_service_1 = require("../../services/cli-control-cluster.service");
|
|
12
12
|
const cluster_node_scaling_service_1 = require("../../../../src/modules/infrastructure/clusters/services/cluster-node-scaling.service");
|
|
13
13
|
const cluster_capacity_service_1 = require("../../../../src/modules/infrastructure/clusters/services/cluster-capacity.service");
|
|
14
14
|
const cli_node_repository_1 = require("../../lib/repositories/cli-node.repository");
|
|
@@ -20,13 +20,13 @@ class EnvScaleNode extends core_1.Command {
|
|
|
20
20
|
const spinner = (0, ora_1.default)('Preparing scale-node plan...').start();
|
|
21
21
|
try {
|
|
22
22
|
const app = await (0, nest_app_1.getNestApp)();
|
|
23
|
-
const
|
|
23
|
+
const controlService = app.get(cli_control_cluster_service_1.CliControlClusterService);
|
|
24
24
|
const nodeRepo = app.get(cli_node_repository_1.CliNodeRepository);
|
|
25
25
|
const capacityService = app.get(cluster_capacity_service_1.ClusterCapacityService);
|
|
26
26
|
const scalingService = app.get(cluster_node_scaling_service_1.ClusterNodeScalingService);
|
|
27
|
-
const cluster = await
|
|
27
|
+
const cluster = await controlService.getControlCluster();
|
|
28
28
|
if (!cluster) {
|
|
29
|
-
spinner.fail('No
|
|
29
|
+
spinner.fail('No control cluster found');
|
|
30
30
|
return;
|
|
31
31
|
}
|
|
32
32
|
const nodes = await nodeRepo.find({ where: { clusterId: cluster.id } });
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
export default class EnvSetMasterProtection extends Command {
|
|
3
|
+
static readonly description: string;
|
|
4
|
+
static readonly examples: string[];
|
|
5
|
+
static readonly args: {
|
|
6
|
+
action: import("@oclif/core/lib/interfaces").Arg<string, Record<string, unknown>>;
|
|
7
|
+
};
|
|
8
|
+
run(): Promise<void>;
|
|
9
|
+
private countWorkers;
|
|
10
|
+
private masterTainted;
|
|
11
|
+
private protectionLabel;
|
|
12
|
+
private showState;
|
|
13
|
+
private turnOn;
|
|
14
|
+
private turnOff;
|
|
15
|
+
private persist;
|
|
16
|
+
}
|