@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
|
@@ -3,12 +3,13 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const node_crypto_1 = require("node:crypto");
|
|
6
7
|
const core_1 = require("@oclif/core");
|
|
7
8
|
const chalk_1 = __importDefault(require("chalk"));
|
|
8
9
|
const ora_1 = __importDefault(require("ora"));
|
|
9
10
|
const nest_app_1 = require("../../lib/nest-app");
|
|
10
11
|
const nip_base_domain_util_1 = require("../../lib/nip-base-domain.util");
|
|
11
|
-
const
|
|
12
|
+
const cli_control_cluster_service_1 = require("../../services/cli-control-cluster.service");
|
|
12
13
|
const cli_ssh_service_1 = require("../../services/cli-ssh.service");
|
|
13
14
|
const cloud_provider_enum_1 = require("../../../../src/modules/providers/enums/cloud-provider.enum");
|
|
14
15
|
const cluster_entity_1 = require("../../../../src/modules/infrastructure/clusters/entities/cluster.entity");
|
|
@@ -21,6 +22,7 @@ const api_client_1 = require("../../lib/api-client");
|
|
|
21
22
|
const config_storage_1 = require("../../lib/config-storage");
|
|
22
23
|
const provider_credential_schemas_1 = require("../../lib/provider-credential-schemas");
|
|
23
24
|
const context_banner_1 = require("../../lib/context-banner");
|
|
25
|
+
const release_override_1 = require("../../config/release-override");
|
|
24
26
|
const server_type_cache_service_1 = require("../../services/server-type-cache.service");
|
|
25
27
|
const server_type_validator_service_1 = require("../../services/server-type-validator.service");
|
|
26
28
|
const defaults_1 = require("../../config/defaults");
|
|
@@ -28,9 +30,36 @@ const prompts_1 = require("../../lib/prompts");
|
|
|
28
30
|
const preferences_resolver_1 = require("../../config/preferences-resolver");
|
|
29
31
|
const preferences_schema_1 = require("../../config/preferences-schema");
|
|
30
32
|
class EnvCreate extends core_1.Command {
|
|
33
|
+
/** The sole provider with configured credentials, or undefined if none/both. */
|
|
34
|
+
detectConfiguredProvider(configStorage) {
|
|
35
|
+
const configured = ['hetzner', 'scaleway'].filter((p) => (0, provider_credential_schemas_1.isCompoundProvider)(p)
|
|
36
|
+
? configStorage.hasCredentials(p)
|
|
37
|
+
: configStorage.hasToken(p));
|
|
38
|
+
return configured.length === 1 ? configured[0] : undefined;
|
|
39
|
+
}
|
|
31
40
|
async run() {
|
|
32
41
|
const { flags } = await this.parse(EnvCreate);
|
|
33
|
-
|
|
42
|
+
if (flags['auth-mode'] !== 'oidc') {
|
|
43
|
+
this.error(`Authentication mode '${flags['auth-mode']}' is not supported. Only 'oidc' is available.`, { exit: 1 });
|
|
44
|
+
}
|
|
45
|
+
const configStorage = new config_storage_1.ConfigStorage();
|
|
46
|
+
const hasCreds = (p) => (0, provider_credential_schemas_1.isCompoundProvider)(p)
|
|
47
|
+
? configStorage.hasCredentials(p)
|
|
48
|
+
: configStorage.hasToken(p);
|
|
49
|
+
// Resolve the target provider BEFORE deriving provider-specific defaults.
|
|
50
|
+
// Precedence: explicit --provider > the profile's single configured
|
|
51
|
+
// provider > the setup wizard's choice. (Previously hard-defaulted to
|
|
52
|
+
// hetzner, so a Scaleway-only profile still tried — and failed — to build
|
|
53
|
+
// on Hetzner.)
|
|
54
|
+
let providerKey = flags.provider ?? this.detectConfiguredProvider(configStorage);
|
|
55
|
+
if (!providerKey || !hasCreds(providerKey)) {
|
|
56
|
+
const configured = await (0, prompts_1.runProviderSetupWizard)(providerKey);
|
|
57
|
+
if (!configured) {
|
|
58
|
+
console.log(chalk_1.default.dim(` To configure manually: flui config set ${providerKey ?? '<provider>'}\n`));
|
|
59
|
+
this.exit(1);
|
|
60
|
+
}
|
|
61
|
+
providerKey = configured;
|
|
62
|
+
}
|
|
34
63
|
const cloudProvider = providerKey === 'scaleway'
|
|
35
64
|
? cloud_provider_enum_1.CloudProvider.SCALEWAY
|
|
36
65
|
: cloud_provider_enum_1.CloudProvider.HETZNER;
|
|
@@ -40,9 +69,6 @@ class EnvCreate extends core_1.Command {
|
|
|
40
69
|
if (!allowedRegions.includes(region)) {
|
|
41
70
|
this.error(`Region '${region}' is not supported for provider '${providerKey}'. Allowed: ${allowedRegions.join(', ')}`, { exit: 1 });
|
|
42
71
|
}
|
|
43
|
-
if (flags['auth-mode'] !== 'oidc') {
|
|
44
|
-
this.error(`Authentication mode '${flags['auth-mode']}' is not supported. Only 'oidc' is available.`, { exit: 1 });
|
|
45
|
-
}
|
|
46
72
|
(0, context_banner_1.printContextBanner)({
|
|
47
73
|
cluster: { provider: providerKey, region },
|
|
48
74
|
});
|
|
@@ -55,9 +81,8 @@ class EnvCreate extends core_1.Command {
|
|
|
55
81
|
app = await (0, nest_app_1.getNestApp)();
|
|
56
82
|
spinner.succeed('Initialized');
|
|
57
83
|
spinner = (0, ora_1.default)('Loading services...').start();
|
|
58
|
-
const configStorage = new config_storage_1.ConfigStorage();
|
|
59
84
|
const apiUrl = configStorage.getApiUrl();
|
|
60
|
-
const
|
|
85
|
+
const controlService = app.get(cli_control_cluster_service_1.CliControlClusterService);
|
|
61
86
|
const apiClient = new api_client_1.ApiClient({
|
|
62
87
|
baseUrl: apiUrl,
|
|
63
88
|
apiKey: configStorage.getApiKey(),
|
|
@@ -65,20 +90,6 @@ class EnvCreate extends core_1.Command {
|
|
|
65
90
|
const cacheService = new server_type_cache_service_1.ServerTypeCacheService();
|
|
66
91
|
const validatorService = new server_type_validator_service_1.ServerTypeValidatorService();
|
|
67
92
|
spinner.succeed('Services loaded');
|
|
68
|
-
// 2. Early credential check — must happen before any provider call
|
|
69
|
-
const hasProviderCreds = (0, provider_credential_schemas_1.isCompoundProvider)(providerKey)
|
|
70
|
-
? configStorage.hasCredentials(providerKey)
|
|
71
|
-
: configStorage.hasToken(providerKey);
|
|
72
|
-
if (!hasProviderCreds) {
|
|
73
|
-
spinner.stop();
|
|
74
|
-
const configured = await (0, prompts_1.runProviderSetupWizard)();
|
|
75
|
-
if (!configured) {
|
|
76
|
-
console.log(chalk_1.default.dim(` To configure manually: flui config set ${providerKey}\n`));
|
|
77
|
-
this.exit(1);
|
|
78
|
-
}
|
|
79
|
-
spinner = (0, ora_1.default)('Resuming...').start();
|
|
80
|
-
spinner.succeed('Provider configured');
|
|
81
|
-
}
|
|
82
93
|
// 3. Resolve admin email (prompt if not set, then persist)
|
|
83
94
|
const emailResolver = new preferences_resolver_1.PreferencesResolver(configStorage);
|
|
84
95
|
const emailResult = emailResolver.resolve('email');
|
|
@@ -102,7 +113,12 @@ class EnvCreate extends core_1.Command {
|
|
|
102
113
|
// server-side, so the LE 5-certs-per-7-days rate limit never trips on
|
|
103
114
|
// repeated test creations.
|
|
104
115
|
const acmeStaging = flags['acme-staging'];
|
|
116
|
+
const useLatest = flags.latest;
|
|
105
117
|
spinner.stop();
|
|
118
|
+
const overrideBanner = (0, release_override_1.formatReleaseOverrideBanner)((0, release_override_1.getEffectiveRelease)(useLatest));
|
|
119
|
+
if (overrideBanner) {
|
|
120
|
+
console.log(overrideBanner);
|
|
121
|
+
}
|
|
106
122
|
if (acmeStaging) {
|
|
107
123
|
console.log(chalk_1.default.yellow(`\n⚠ ACME endpoint: Let's Encrypt STAGING — cert will not be browser-trusted (warning expected).\n`));
|
|
108
124
|
}
|
|
@@ -231,17 +247,17 @@ class EnvCreate extends core_1.Command {
|
|
|
231
247
|
spinner.succeed('Server type validation skipped (no data available)');
|
|
232
248
|
}
|
|
233
249
|
// Display cluster configuration
|
|
234
|
-
console.log(chalk_1.default.cyan('\n🚀 Creating Flui
|
|
250
|
+
console.log(chalk_1.default.cyan('\n🚀 Creating Flui Control Cluster (K3s)\n'));
|
|
235
251
|
console.log(chalk_1.default.dim(` Provider: ${providerKey}`));
|
|
236
252
|
console.log(chalk_1.default.dim(` Node Size: ${validatedNodeSize}`));
|
|
237
253
|
console.log(chalk_1.default.dim(` Region: ${validatedRegion}`));
|
|
238
254
|
console.log(chalk_1.default.dim(` Worker Nodes: ${flags['node-count']}\n`));
|
|
239
|
-
// 3. Check if
|
|
255
|
+
// 3. Check if control cluster already exists
|
|
240
256
|
spinner = (0, ora_1.default)('Checking for existing cluster...').start();
|
|
241
|
-
const existingCluster = await
|
|
257
|
+
const existingCluster = await controlService.getControlCluster();
|
|
242
258
|
if (existingCluster && existingCluster.status !== cluster_entity_1.ClusterStatus.DELETED) {
|
|
243
|
-
spinner.fail('
|
|
244
|
-
console.log(chalk_1.default.yellow('\n⚠️ An
|
|
259
|
+
spinner.fail('Control cluster already exists!');
|
|
260
|
+
console.log(chalk_1.default.yellow('\n⚠️ An control cluster is already running:\n'));
|
|
245
261
|
console.log(` ${chalk_1.default.bold('Name:')} ${existingCluster.name}`);
|
|
246
262
|
console.log(` ${chalk_1.default.bold('ID:')} ${existingCluster.id}`);
|
|
247
263
|
console.log(` ${chalk_1.default.bold('Status:')} ${existingCluster.status}`);
|
|
@@ -263,7 +279,7 @@ class EnvCreate extends core_1.Command {
|
|
|
263
279
|
const vnetService = app.get(vnet_provisioning_service_1.VnetProvisioningService);
|
|
264
280
|
envVnetInfo = await vnetService.ensureEnvVnet({
|
|
265
281
|
provider: cloudProvider,
|
|
266
|
-
name:
|
|
282
|
+
name: `flui-env-${(0, node_crypto_1.randomBytes)(3).toString('hex')}`,
|
|
267
283
|
ipRange: '10.10.0.0/16',
|
|
268
284
|
subnetIpRange: '10.10.1.0/24',
|
|
269
285
|
networkZone: cloudProvider === cloud_provider_enum_1.CloudProvider.HETZNER
|
|
@@ -277,7 +293,7 @@ class EnvCreate extends core_1.Command {
|
|
|
277
293
|
}
|
|
278
294
|
catch (error) {
|
|
279
295
|
spinner.fail(`VNet provisioning failed: ${error.message}`);
|
|
280
|
-
console.log(chalk_1.default.red('\n Cannot proceed without an environment VNet. The
|
|
296
|
+
console.log(chalk_1.default.red('\n Cannot proceed without an environment VNet. The control cluster and all workload clusters must share a private network.'));
|
|
281
297
|
this.exit(1);
|
|
282
298
|
}
|
|
283
299
|
// 4. Create firewall BEFORE cluster (if enabled)
|
|
@@ -296,15 +312,18 @@ class EnvCreate extends core_1.Command {
|
|
|
296
312
|
const publicIp = await ipService.getPublicIp();
|
|
297
313
|
sourceCidrs = [ipService.toCidr(publicIp)];
|
|
298
314
|
}
|
|
299
|
-
const rules = (0, firewall_rules_1.
|
|
300
|
-
|
|
315
|
+
const rules = (0, firewall_rules_1.CONTROL_FIREWALL_RULES)(sourceCidrs);
|
|
316
|
+
// Unique temporary name to avoid colliding with orphaned firewalls
|
|
317
|
+
// from previous runs; renamed to flui-control-firewall-<clusterId>
|
|
318
|
+
// (by firewallId) once the cluster is ready.
|
|
319
|
+
const temporaryFirewallName = `flui-control-firewall-${(0, node_crypto_1.randomBytes)(4).toString('hex')}`;
|
|
301
320
|
// Create firewall with temporary name (will be renamed when cluster is ready)
|
|
302
321
|
const result = await firewallService.createFirewall({
|
|
303
322
|
name: temporaryFirewallName,
|
|
304
323
|
labels: [
|
|
305
324
|
{ key: 'managed-by', value: 'flui-cloud' },
|
|
306
325
|
{ key: 'flui-resource-type', value: 'firewall' },
|
|
307
|
-
{ key: 'flui-cluster-type', value: '
|
|
326
|
+
{ key: 'flui-cluster-type', value: 'control' },
|
|
308
327
|
],
|
|
309
328
|
rules,
|
|
310
329
|
// Label selector will be applied later when cluster servers exist
|
|
@@ -326,10 +345,10 @@ class EnvCreate extends core_1.Command {
|
|
|
326
345
|
spinner.succeed(`Firewall created (Source: ${sourceCidrs.join(', ')})`);
|
|
327
346
|
}
|
|
328
347
|
catch (error) {
|
|
329
|
-
spinner.
|
|
330
|
-
console.log(chalk_1.default.
|
|
331
|
-
|
|
332
|
-
|
|
348
|
+
spinner.fail(`Firewall creation failed: ${error.message}`);
|
|
349
|
+
console.log(chalk_1.default.red('\n Aborting: the control cluster must not be provisioned without a firewall.'));
|
|
350
|
+
console.log(chalk_1.default.dim(' Resolve the cause (e.g. remove a leftover/orphaned firewall on the provider) and retry,\n or pass --no-configure-firewall to explicitly opt out of firewall protection.'));
|
|
351
|
+
this.exit(1);
|
|
333
352
|
}
|
|
334
353
|
}
|
|
335
354
|
// 5. Create K3s cluster
|
|
@@ -337,7 +356,7 @@ class EnvCreate extends core_1.Command {
|
|
|
337
356
|
console.log(chalk_1.default.dim(flags['node-count'] > 0
|
|
338
357
|
? ' This will create master node + worker nodes'
|
|
339
358
|
: ' This will create master node only'));
|
|
340
|
-
const clusterId = await
|
|
359
|
+
const clusterId = await controlService.createControlCluster(cloudProvider, validatedRegion, validatedNodeSize, flags['node-count'], firewallId, sourceCidrs, flags['auth-mode'], envVnetInfo
|
|
341
360
|
? {
|
|
342
361
|
vnetProviderResourceId: envVnetInfo.vnetProviderResourceId,
|
|
343
362
|
vnetIpRange: envVnetInfo.vnetIpRange,
|
|
@@ -349,6 +368,7 @@ class EnvCreate extends core_1.Command {
|
|
|
349
368
|
: undefined, adminEmail, acmeStaging, flags['disk-size'], {
|
|
350
369
|
sharedStorageEnabled: !flags['no-shared-storage'],
|
|
351
370
|
sharedStorageVolumeSizeGb: flags['shared-storage-size'],
|
|
371
|
+
useLatest,
|
|
352
372
|
});
|
|
353
373
|
spinner.succeed('Cluster creation started!');
|
|
354
374
|
if (firewallId && clusterId) {
|
|
@@ -369,7 +389,7 @@ class EnvCreate extends core_1.Command {
|
|
|
369
389
|
}
|
|
370
390
|
if (flags.detached) {
|
|
371
391
|
// DETACHED MODE: Exit immediately with monitoring instructions
|
|
372
|
-
console.log(chalk_1.default.green('\n✅
|
|
392
|
+
console.log(chalk_1.default.green('\n✅ Control Cluster Creation Started!\n'));
|
|
373
393
|
console.log(chalk_1.default.cyan('📋 Cluster Details:\n'));
|
|
374
394
|
console.log(` ${chalk_1.default.bold('Cluster ID:')} ${clusterId}`);
|
|
375
395
|
console.log(` ${chalk_1.default.bold('Provider:')} ${cloudProvider}`);
|
|
@@ -391,7 +411,7 @@ class EnvCreate extends core_1.Command {
|
|
|
391
411
|
// WAIT MODE: Wait for cluster to be ready
|
|
392
412
|
spinner = (0, ora_1.default)('Waiting for cluster to be ready...').start();
|
|
393
413
|
try {
|
|
394
|
-
await
|
|
414
|
+
await controlService.waitForClusterReady(clusterId, 600000);
|
|
395
415
|
spinner.succeed('Cluster is ready!');
|
|
396
416
|
}
|
|
397
417
|
catch (error) {
|
|
@@ -399,7 +419,7 @@ class EnvCreate extends core_1.Command {
|
|
|
399
419
|
console.log(chalk_1.default.red(`\n❌ Error: ${error.message}\n`));
|
|
400
420
|
this.exit(1);
|
|
401
421
|
}
|
|
402
|
-
console.log(chalk_1.default.green('\n✅
|
|
422
|
+
console.log(chalk_1.default.green('\n✅ Control Cluster Created Successfully!\n'));
|
|
403
423
|
console.log(chalk_1.default.cyan('📋 Cluster Details:\n'));
|
|
404
424
|
console.log(` ${chalk_1.default.bold('Cluster ID:')} ${clusterId}`);
|
|
405
425
|
console.log(` ${chalk_1.default.bold('Provider:')} ${cloudProvider}`);
|
|
@@ -428,7 +448,7 @@ class EnvCreate extends core_1.Command {
|
|
|
428
448
|
// This ensures reconciliation runs even if SSH setup fails
|
|
429
449
|
let shouldExit = false;
|
|
430
450
|
const onReconcile = async () => {
|
|
431
|
-
const cluster = await
|
|
451
|
+
const cluster = await controlService.getControlCluster();
|
|
432
452
|
const masterIp = cluster?.masterIpAddress;
|
|
433
453
|
const nipToken = cluster?.nipHostnameToken;
|
|
434
454
|
const baseDomain = (0, nip_base_domain_util_1.buildNipBaseDomain)(masterIp ?? '', nipToken);
|
|
@@ -440,13 +460,13 @@ class EnvCreate extends core_1.Command {
|
|
|
440
460
|
const interval = 15_000;
|
|
441
461
|
const deadline = Date.now() + maxWait;
|
|
442
462
|
while (Date.now() < deadline) {
|
|
443
|
-
const valid = await
|
|
463
|
+
const valid = await controlService.waitForValidTls(certUrl, interval, interval, acmeStaging);
|
|
444
464
|
if (valid)
|
|
445
465
|
break;
|
|
446
466
|
elapsed += interval;
|
|
447
467
|
console.log(chalk_1.default.dim(` Certificate pending... (${Math.round(elapsed / 1000)}s / ${maxWait / 1000}s)`));
|
|
448
468
|
}
|
|
449
|
-
const tlsReady = await
|
|
469
|
+
const tlsReady = await controlService.waitForValidTls(certUrl, 5_000, 5_000, acmeStaging);
|
|
450
470
|
if (tlsReady) {
|
|
451
471
|
console.log(chalk_1.default.green(acmeStaging
|
|
452
472
|
? "✅ TLS certificate valid (Let's Encrypt STAGING — not browser-trusted)"
|
|
@@ -477,12 +497,14 @@ class EnvCreate extends core_1.Command {
|
|
|
477
497
|
}
|
|
478
498
|
}
|
|
479
499
|
console.log(chalk_1.default.dim(`\n→ Waiting for OIDC client provisioning...`));
|
|
480
|
-
const oidcReady = await
|
|
500
|
+
const oidcReady = await controlService.waitForOidcReady(apiBaseUrl, 300_000, 10_000, acmeStaging);
|
|
481
501
|
if (oidcReady) {
|
|
482
502
|
console.log(chalk_1.default.green('✅ OIDC login ready'));
|
|
483
503
|
}
|
|
484
504
|
else {
|
|
485
|
-
console.log(chalk_1.default.yellow('⚠ OIDC not ready —
|
|
505
|
+
console.log(chalk_1.default.yellow('⚠ OIDC login not ready — the dashboard sign-in will fail until this is resolved.'));
|
|
506
|
+
console.log(chalk_1.default.dim(' Diagnose with `flui env status`, or `flui ssh master` + kubectl (check the zitadel and flui-web pods).\n' +
|
|
507
|
+
' `flui env force-ready` does NOT retry bootstrap — it only overrides the local status, so use it only after the cause is fixed.'));
|
|
486
508
|
}
|
|
487
509
|
}
|
|
488
510
|
}
|
|
@@ -507,12 +529,12 @@ class EnvCreate extends core_1.Command {
|
|
|
507
529
|
'\n'));
|
|
508
530
|
shouldExit = true;
|
|
509
531
|
};
|
|
510
|
-
pollerHandle =
|
|
532
|
+
pollerHandle = controlService.pollOperationUntilReady(clusterId, onReconcile, onFailed);
|
|
511
533
|
// Phase 1: Wait for server provisioning (master IP)
|
|
512
534
|
spinner = (0, ora_1.default)('Provisioning server...').start();
|
|
513
535
|
let masterIp;
|
|
514
536
|
try {
|
|
515
|
-
masterIp = await
|
|
537
|
+
masterIp = await controlService.waitForMasterIp(clusterId, 600000, 5000);
|
|
516
538
|
spinner.succeed(`Server provisioned (${masterIp})`);
|
|
517
539
|
}
|
|
518
540
|
catch (error) {
|
|
@@ -528,7 +550,7 @@ class EnvCreate extends core_1.Command {
|
|
|
528
550
|
// Phase 2: Wait for server boot (SSH port open)
|
|
529
551
|
spinner = (0, ora_1.default)('Server booting...').start();
|
|
530
552
|
try {
|
|
531
|
-
await
|
|
553
|
+
await controlService.waitForPortReady(masterIp, 22, 600000, 5000);
|
|
532
554
|
spinner.succeed('Server online');
|
|
533
555
|
}
|
|
534
556
|
catch (error) {
|
|
@@ -542,7 +564,7 @@ class EnvCreate extends core_1.Command {
|
|
|
542
564
|
// Phase 3: Wait for SSH authentication (CA enrollment via cloud-init)
|
|
543
565
|
spinner = (0, ora_1.default)('Waiting for SSH access (CA enrollment)...').start();
|
|
544
566
|
try {
|
|
545
|
-
await
|
|
567
|
+
await controlService.waitForSshAuth(() => sshService.sshExec(masterIp, 'echo ok'), 600000, 10000);
|
|
546
568
|
spinner.succeed('SSH access ready');
|
|
547
569
|
}
|
|
548
570
|
catch (error) {
|
|
@@ -611,7 +633,7 @@ class EnvCreate extends core_1.Command {
|
|
|
611
633
|
}
|
|
612
634
|
}
|
|
613
635
|
catch (error) {
|
|
614
|
-
spinner.fail('Failed to create
|
|
636
|
+
spinner.fail('Failed to create control cluster');
|
|
615
637
|
if (firewallId) {
|
|
616
638
|
try {
|
|
617
639
|
spinner = (0, ora_1.default)('Cleaning up orphaned firewall...').start();
|
|
@@ -653,7 +675,7 @@ class EnvCreate extends core_1.Command {
|
|
|
653
675
|
}
|
|
654
676
|
}
|
|
655
677
|
}
|
|
656
|
-
EnvCreate.description = 'Create
|
|
678
|
+
EnvCreate.description = 'Create control cluster infrastructure on K3s';
|
|
657
679
|
EnvCreate.examples = [
|
|
658
680
|
'<%= config.bin %> <%= command.id %>',
|
|
659
681
|
'<%= config.bin %> <%= command.id %> --node-size cx32',
|
|
@@ -664,8 +686,7 @@ EnvCreate.examples = [
|
|
|
664
686
|
EnvCreate.flags = {
|
|
665
687
|
provider: core_1.Flags.string({
|
|
666
688
|
char: 'p',
|
|
667
|
-
description:
|
|
668
|
-
default: 'hetzner',
|
|
689
|
+
description: "Cloud provider for the control cluster (default: the profile's configured provider)",
|
|
669
690
|
options: ['hetzner', 'scaleway'],
|
|
670
691
|
}),
|
|
671
692
|
'node-size': core_1.Flags.string({
|
|
@@ -696,6 +717,7 @@ EnvCreate.flags = {
|
|
|
696
717
|
'configure-firewall': core_1.Flags.boolean({
|
|
697
718
|
description: 'Automatically configure firewall after cluster creation',
|
|
698
719
|
default: true,
|
|
720
|
+
allowNo: true,
|
|
699
721
|
}),
|
|
700
722
|
'firewall-ip': core_1.Flags.string({
|
|
701
723
|
description: 'Source IP/CIDR for firewall (comma-separated, default: auto-detect)',
|
|
@@ -708,6 +730,10 @@ EnvCreate.flags = {
|
|
|
708
730
|
description: "Use Let's Encrypt staging endpoint (untrusted cert, no rate limits) — useful while iterating to avoid burning prod quota",
|
|
709
731
|
default: false,
|
|
710
732
|
}),
|
|
733
|
+
latest: core_1.Flags.boolean({
|
|
734
|
+
description: 'Install mobile dev tags instead of the pinned release: bootstrap scripts from `master` and `:latest` Docker images. Default: every component is pinned to the CLI release version.',
|
|
735
|
+
default: false,
|
|
736
|
+
}),
|
|
711
737
|
'no-shared-storage': core_1.Flags.boolean({
|
|
712
738
|
description: 'Disable Flui shared storage (NFS+fscache). Default: shared storage enabled — master gets a Volume hosting the NFS export, workers mount it. Disable to fall back to local-path on each node bundled disk.',
|
|
713
739
|
default: false,
|
|
@@ -45,7 +45,7 @@ const os = __importStar(require("node:os"));
|
|
|
45
45
|
const nest_app_1 = require("../../lib/nest-app");
|
|
46
46
|
const context_banner_1 = require("../../lib/context-banner");
|
|
47
47
|
const nip_base_domain_util_1 = require("../../lib/nip-base-domain.util");
|
|
48
|
-
const
|
|
48
|
+
const cli_control_cluster_service_1 = require("../../services/cli-control-cluster.service");
|
|
49
49
|
const cli_ssh_service_1 = require("../../services/cli-ssh.service");
|
|
50
50
|
const encryption_service_1 = require("../../../../src/modules/shared/encryption/services/encryption.service");
|
|
51
51
|
const cluster_entity_1 = require("../../../../src/modules/infrastructure/clusters/entities/cluster.entity");
|
|
@@ -95,9 +95,9 @@ class EnvCredentials extends core_1.Command {
|
|
|
95
95
|
return '';
|
|
96
96
|
}
|
|
97
97
|
}
|
|
98
|
-
async runHealthCheck(
|
|
98
|
+
async runHealthCheck(controlService, masterIp, nipHostnameToken) {
|
|
99
99
|
const s = (0, ora_1.default)('Testing connections...').start();
|
|
100
|
-
const result = await
|
|
100
|
+
const result = await controlService.checkObservabilityServices(masterIp, nipHostnameToken);
|
|
101
101
|
s.succeed('Connection tests completed');
|
|
102
102
|
return result;
|
|
103
103
|
}
|
|
@@ -141,13 +141,13 @@ class EnvCredentials extends core_1.Command {
|
|
|
141
141
|
let spinner = (0, ora_1.default)('Fetching credentials...').start();
|
|
142
142
|
try {
|
|
143
143
|
const app = await (0, nest_app_1.getNestApp)();
|
|
144
|
-
const
|
|
144
|
+
const controlService = app.get(cli_control_cluster_service_1.CliControlClusterService);
|
|
145
145
|
const encryptionService = app.get(encryption_service_1.EncryptionService);
|
|
146
|
-
// Get
|
|
147
|
-
const cluster = await
|
|
146
|
+
// Get control cluster
|
|
147
|
+
const cluster = await controlService.getControlCluster();
|
|
148
148
|
if (!cluster) {
|
|
149
|
-
spinner.fail('No
|
|
150
|
-
console.log(chalk_1.default.yellow('\n⚠️ No
|
|
149
|
+
spinner.fail('No control cluster found');
|
|
150
|
+
console.log(chalk_1.default.yellow('\n⚠️ No control cluster exists.\n'));
|
|
151
151
|
console.log(chalk_1.default.dim('Create one with:'));
|
|
152
152
|
console.log(` ${chalk_1.default.cyan('flui env create')}\n`);
|
|
153
153
|
return;
|
|
@@ -159,7 +159,7 @@ class EnvCredentials extends core_1.Command {
|
|
|
159
159
|
}
|
|
160
160
|
spinner.succeed('Cluster found');
|
|
161
161
|
// Get endpoints
|
|
162
|
-
const endpoints = await
|
|
162
|
+
const endpoints = await controlService.getObservabilityEndpoints(cluster.id);
|
|
163
163
|
const masterIp = cluster.masterIpAddress;
|
|
164
164
|
if (!masterIp) {
|
|
165
165
|
spinner.fail('Master IP address not available');
|
|
@@ -204,7 +204,7 @@ class EnvCredentials extends core_1.Command {
|
|
|
204
204
|
? this.redact(zitadelAdminTempPassword, showSecrets)
|
|
205
205
|
: '';
|
|
206
206
|
const healthStatus = flags.test
|
|
207
|
-
? await this.runHealthCheck(
|
|
207
|
+
? await this.runHealthCheck(controlService, masterIp, cluster.nipHostnameToken)
|
|
208
208
|
: null;
|
|
209
209
|
const secretStatus = flags.verify
|
|
210
210
|
? await this.runSecretVerify(app.get(cli_ssh_service_1.CliSshService), masterIp)
|
|
@@ -295,7 +295,7 @@ class EnvCredentials extends core_1.Command {
|
|
|
295
295
|
}
|
|
296
296
|
}
|
|
297
297
|
displayTextOutput(masterIp, endpoints, passwords, admin, healthStatus, secretStatus, zitadel = null) {
|
|
298
|
-
console.log(chalk_1.default.cyan('\n📋
|
|
298
|
+
console.log(chalk_1.default.cyan('\n📋 Control Cluster Credentials'));
|
|
299
299
|
console.log(chalk_1.default.cyan('━'.repeat(50)));
|
|
300
300
|
// Endpoints section
|
|
301
301
|
console.log(chalk_1.default.cyan('\n🌐 Endpoints:\n'));
|
|
@@ -404,7 +404,7 @@ class EnvCredentials extends core_1.Command {
|
|
|
404
404
|
return `${chalk_1.default.red('❌ unreachable')}`;
|
|
405
405
|
}
|
|
406
406
|
}
|
|
407
|
-
EnvCredentials.description = 'Display
|
|
407
|
+
EnvCredentials.description = 'Display control cluster connection information.\n' +
|
|
408
408
|
'Secrets are hidden by default; pass --show-secrets to print them. To populate\n' +
|
|
409
409
|
'a local .env.local for development use `flui dev creds` instead.';
|
|
410
410
|
EnvCredentials.examples = [
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { Command } from '@oclif/core';
|
|
2
2
|
export default class EnvDestroy extends Command {
|
|
3
|
-
static readonly description = "Permanently delete
|
|
3
|
+
static readonly description = "Permanently delete control cluster (WARNING: All data will be lost!)";
|
|
4
4
|
static readonly examples: string[];
|
|
5
5
|
static readonly flags: {
|
|
6
6
|
force: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
7
7
|
};
|
|
8
|
+
private cleanupClusterScopedConfig;
|
|
8
9
|
run(): Promise<void>;
|
|
9
10
|
}
|
|
@@ -8,11 +8,45 @@ 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 config_storage_1 = require("../../lib/config-storage");
|
|
13
13
|
const prompts_1 = require("../../lib/prompts");
|
|
14
14
|
const vnet_provisioning_service_1 = require("../../lib/services/vnet-provisioning.service");
|
|
15
15
|
class EnvDestroy extends core_1.Command {
|
|
16
|
+
// Drop config tied to the deleted cluster: registration, API endpoint and key.
|
|
17
|
+
cleanupClusterScopedConfig() {
|
|
18
|
+
const spinner = (0, ora_1.default)('Cleaning up configuration...').start();
|
|
19
|
+
try {
|
|
20
|
+
const configStorage = new config_storage_1.ConfigStorage();
|
|
21
|
+
const config = configStorage['readConfig']();
|
|
22
|
+
let changed = false;
|
|
23
|
+
if (config.credentials?.['observability-cluster-registration']) {
|
|
24
|
+
delete config.credentials['observability-cluster-registration'];
|
|
25
|
+
changed = true;
|
|
26
|
+
}
|
|
27
|
+
if (config.apiUrl) {
|
|
28
|
+
delete config.apiUrl;
|
|
29
|
+
if (config.metadata)
|
|
30
|
+
delete config.metadata.apiUrlUpdatedAt;
|
|
31
|
+
changed = true;
|
|
32
|
+
}
|
|
33
|
+
if (config.apiKey) {
|
|
34
|
+
delete config.apiKey;
|
|
35
|
+
changed = true;
|
|
36
|
+
}
|
|
37
|
+
if (changed) {
|
|
38
|
+
configStorage['writeConfig'](config);
|
|
39
|
+
spinner.succeed('Configuration cleaned up');
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
spinner.info('No configuration to clean up');
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
spinner.warn(`Failed to clean up configuration: ${error.message}`);
|
|
47
|
+
console.log(chalk_1.default.yellow(' This is not critical, continuing...'));
|
|
48
|
+
}
|
|
49
|
+
}
|
|
16
50
|
async run() {
|
|
17
51
|
const { flags } = await this.parse(EnvDestroy);
|
|
18
52
|
(0, context_banner_1.printContextBanner)();
|
|
@@ -21,14 +55,14 @@ class EnvDestroy extends core_1.Command {
|
|
|
21
55
|
// Bootstrap NestJS and get services
|
|
22
56
|
const app = await (0, nest_app_1.getNestApp)();
|
|
23
57
|
spinner.stop();
|
|
24
|
-
console.log(chalk_1.default.red('\n⚠️ DESTROY
|
|
58
|
+
console.log(chalk_1.default.red('\n⚠️ DESTROY Control Cluster\n'));
|
|
25
59
|
spinner = (0, ora_1.default)('Checking for cluster...').start();
|
|
26
|
-
const
|
|
27
|
-
// Find
|
|
28
|
-
const cluster = await
|
|
60
|
+
const controlService = app.get(cli_control_cluster_service_1.CliControlClusterService);
|
|
61
|
+
// Find control cluster
|
|
62
|
+
const cluster = await controlService.getControlCluster();
|
|
29
63
|
if (!cluster) {
|
|
30
|
-
spinner.fail('No
|
|
31
|
-
console.log(chalk_1.default.yellow('\n⚠️ No
|
|
64
|
+
spinner.fail('No control cluster found');
|
|
65
|
+
console.log(chalk_1.default.yellow('\n⚠️ No control cluster exists.\n'));
|
|
32
66
|
console.log(chalk_1.default.dim('Create one with:'));
|
|
33
67
|
console.log(` ${chalk_1.default.cyan('flui env create')}\n`);
|
|
34
68
|
return;
|
|
@@ -64,31 +98,14 @@ class EnvDestroy extends core_1.Command {
|
|
|
64
98
|
color: 'yellow',
|
|
65
99
|
}).start();
|
|
66
100
|
try {
|
|
67
|
-
await
|
|
101
|
+
await controlService.deleteControlCluster();
|
|
68
102
|
spinner.succeed('All cluster resources deleted successfully');
|
|
69
103
|
}
|
|
70
104
|
catch (error) {
|
|
71
105
|
spinner.fail('Cluster deletion encountered an error');
|
|
72
106
|
throw error;
|
|
73
107
|
}
|
|
74
|
-
|
|
75
|
-
spinner = (0, ora_1.default)('Cleaning up configuration...').start();
|
|
76
|
-
try {
|
|
77
|
-
const configStorage = new config_storage_1.ConfigStorage();
|
|
78
|
-
const config = configStorage['readConfig']();
|
|
79
|
-
if (config.credentials?.['observability-cluster-registration']) {
|
|
80
|
-
delete config.credentials['observability-cluster-registration'];
|
|
81
|
-
configStorage['writeConfig'](config);
|
|
82
|
-
spinner.succeed('Configuration cleaned up');
|
|
83
|
-
}
|
|
84
|
-
else {
|
|
85
|
-
spinner.info('No registration to clean up');
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
catch (error) {
|
|
89
|
-
spinner.warn(`Failed to clean up configuration: ${error.message}`);
|
|
90
|
-
console.log(chalk_1.default.yellow(' This is not critical, continuing...'));
|
|
91
|
-
}
|
|
108
|
+
this.cleanupClusterScopedConfig();
|
|
92
109
|
// Tear down environment VNet/Subnet (must run AFTER all servers are deleted —
|
|
93
110
|
// Hetzner refuses to delete a network that still has attached servers).
|
|
94
111
|
try {
|
|
@@ -101,7 +118,7 @@ class EnvDestroy extends core_1.Command {
|
|
|
101
118
|
spinner.warn(`VNet teardown failed: ${error.message}`);
|
|
102
119
|
console.log(chalk_1.default.yellow(' You may need to delete the VNet manually from the Hetzner console.'));
|
|
103
120
|
}
|
|
104
|
-
console.log(chalk_1.default.green('\n✅
|
|
121
|
+
console.log(chalk_1.default.green('\n✅ Control Cluster Deleted Successfully\n'));
|
|
105
122
|
console.log(chalk_1.default.dim(' All cluster resources have been removed:'));
|
|
106
123
|
console.log(chalk_1.default.dim(' • Servers (master and worker nodes)'));
|
|
107
124
|
console.log(chalk_1.default.dim(' • Firewalls and security rules'));
|
|
@@ -130,7 +147,7 @@ class EnvDestroy extends core_1.Command {
|
|
|
130
147
|
}
|
|
131
148
|
}
|
|
132
149
|
}
|
|
133
|
-
EnvDestroy.description = 'Permanently delete
|
|
150
|
+
EnvDestroy.description = 'Permanently delete control cluster (WARNING: All data will be lost!)';
|
|
134
151
|
EnvDestroy.examples = [
|
|
135
152
|
'<%= config.bin %> <%= command.id %>',
|
|
136
153
|
'<%= config.bin %> <%= command.id %> --force',
|
|
@@ -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 nip_base_domain_util_1 = require("../../lib/nip-base-domain.util");
|
|
11
|
-
const
|
|
11
|
+
const cli_control_cluster_service_1 = require("../../services/cli-control-cluster.service");
|
|
12
12
|
const cli_ssh_service_1 = require("../../services/cli-ssh.service");
|
|
13
13
|
const context_banner_1 = require("../../lib/context-banner");
|
|
14
14
|
class EnvDiagCA extends core_1.Command {
|
|
@@ -17,9 +17,9 @@ class EnvDiagCA extends core_1.Command {
|
|
|
17
17
|
const spinner = (0, ora_1.default)('Connecting to cluster...').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
|
-
const cluster = await
|
|
22
|
+
const cluster = await controlService.getControlCluster();
|
|
23
23
|
if (!this.assertClusterReady(cluster, spinner)) {
|
|
24
24
|
return;
|
|
25
25
|
}
|
|
@@ -42,8 +42,8 @@ class EnvDiagCA extends core_1.Command {
|
|
|
42
42
|
}
|
|
43
43
|
assertClusterReady(cluster, spinner) {
|
|
44
44
|
if (!cluster) {
|
|
45
|
-
spinner.fail('No
|
|
46
|
-
console.log(chalk_1.default.yellow('\n⚠️ No
|
|
45
|
+
spinner.fail('No control cluster found');
|
|
46
|
+
console.log(chalk_1.default.yellow('\n⚠️ No control cluster exists.\n'));
|
|
47
47
|
console.log(chalk_1.default.dim('Create one with:'));
|
|
48
48
|
console.log(` ${chalk_1.default.cyan('flui env create')}\n`);
|
|
49
49
|
return false;
|
|
@@ -14,25 +14,8 @@ export default class EnvExportConfig extends Command {
|
|
|
14
14
|
save: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
15
15
|
};
|
|
16
16
|
run(): Promise<void>;
|
|
17
|
-
/**
|
|
18
|
-
* Resolve every preference this command consumes (email, dashboardPath, certificateMode)
|
|
19
|
-
* through the layered config: flag > env > project-local > user > default > prompt.
|
|
20
|
-
*
|
|
21
|
-
* Prompted values are persisted to the active profile only when --save is set;
|
|
22
|
-
* otherwise they are used for this run and forgotten (no surprise side effects).
|
|
23
|
-
*
|
|
24
|
-
* `skipDashboard` shortcuts the dashboard-only preferences when --no-dashboard is set.
|
|
25
|
-
*/
|
|
26
17
|
private resolvePreferences;
|
|
27
|
-
/**
|
|
28
|
-
* Patch the dashboard's config.json with the localhost API endpoints + cluster auth +
|
|
29
|
-
* the resolved certificateMode. Caller is responsible for resolving inputs.
|
|
30
|
-
*/
|
|
31
18
|
private syncDashboardConfig;
|
|
32
|
-
/**
|
|
33
|
-
* Prompt for a missing preference value, validating against the schema.
|
|
34
|
-
* Used only when every other layer (flag, env, project, user, default) failed to provide a value.
|
|
35
|
-
*/
|
|
36
19
|
private promptForPreference;
|
|
37
20
|
private resolveIssuer;
|
|
38
21
|
private resolveJwks;
|