@flui-cloud/cli 0.0.1 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +73 -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 +45 -44
- 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 +85 -0
- package/lib/cli/src/config/bootstrap.config.d.ts +10 -1
- package/lib/cli/src/config/bootstrap.config.js +21 -4
- package/lib/cli/src/config/preferences-schema.js +5 -5
- 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 +544 -0
- package/lib/cli/src/services/cli-endpoint-resolver.service.d.ts +1 -0
- package/lib/cli/src/services/cli-endpoint-resolver.service.js +8 -2
- 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 +1025 -678
- 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");
|
|
@@ -28,9 +29,36 @@ const prompts_1 = require("../../lib/prompts");
|
|
|
28
29
|
const preferences_resolver_1 = require("../../config/preferences-resolver");
|
|
29
30
|
const preferences_schema_1 = require("../../config/preferences-schema");
|
|
30
31
|
class EnvCreate extends core_1.Command {
|
|
32
|
+
/** The sole provider with configured credentials, or undefined if none/both. */
|
|
33
|
+
detectConfiguredProvider(configStorage) {
|
|
34
|
+
const configured = ['hetzner', 'scaleway'].filter((p) => (0, provider_credential_schemas_1.isCompoundProvider)(p)
|
|
35
|
+
? configStorage.hasCredentials(p)
|
|
36
|
+
: configStorage.hasToken(p));
|
|
37
|
+
return configured.length === 1 ? configured[0] : undefined;
|
|
38
|
+
}
|
|
31
39
|
async run() {
|
|
32
40
|
const { flags } = await this.parse(EnvCreate);
|
|
33
|
-
|
|
41
|
+
if (flags['auth-mode'] !== 'oidc') {
|
|
42
|
+
this.error(`Authentication mode '${flags['auth-mode']}' is not supported. Only 'oidc' is available.`, { exit: 1 });
|
|
43
|
+
}
|
|
44
|
+
const configStorage = new config_storage_1.ConfigStorage();
|
|
45
|
+
const hasCreds = (p) => (0, provider_credential_schemas_1.isCompoundProvider)(p)
|
|
46
|
+
? configStorage.hasCredentials(p)
|
|
47
|
+
: configStorage.hasToken(p);
|
|
48
|
+
// Resolve the target provider BEFORE deriving provider-specific defaults.
|
|
49
|
+
// Precedence: explicit --provider > the profile's single configured
|
|
50
|
+
// provider > the setup wizard's choice. (Previously hard-defaulted to
|
|
51
|
+
// hetzner, so a Scaleway-only profile still tried — and failed — to build
|
|
52
|
+
// on Hetzner.)
|
|
53
|
+
let providerKey = flags.provider ?? this.detectConfiguredProvider(configStorage);
|
|
54
|
+
if (!providerKey || !hasCreds(providerKey)) {
|
|
55
|
+
const configured = await (0, prompts_1.runProviderSetupWizard)(providerKey);
|
|
56
|
+
if (!configured) {
|
|
57
|
+
console.log(chalk_1.default.dim(` To configure manually: flui config set ${providerKey ?? '<provider>'}\n`));
|
|
58
|
+
this.exit(1);
|
|
59
|
+
}
|
|
60
|
+
providerKey = configured;
|
|
61
|
+
}
|
|
34
62
|
const cloudProvider = providerKey === 'scaleway'
|
|
35
63
|
? cloud_provider_enum_1.CloudProvider.SCALEWAY
|
|
36
64
|
: cloud_provider_enum_1.CloudProvider.HETZNER;
|
|
@@ -40,9 +68,6 @@ class EnvCreate extends core_1.Command {
|
|
|
40
68
|
if (!allowedRegions.includes(region)) {
|
|
41
69
|
this.error(`Region '${region}' is not supported for provider '${providerKey}'. Allowed: ${allowedRegions.join(', ')}`, { exit: 1 });
|
|
42
70
|
}
|
|
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
71
|
(0, context_banner_1.printContextBanner)({
|
|
47
72
|
cluster: { provider: providerKey, region },
|
|
48
73
|
});
|
|
@@ -55,9 +80,8 @@ class EnvCreate extends core_1.Command {
|
|
|
55
80
|
app = await (0, nest_app_1.getNestApp)();
|
|
56
81
|
spinner.succeed('Initialized');
|
|
57
82
|
spinner = (0, ora_1.default)('Loading services...').start();
|
|
58
|
-
const configStorage = new config_storage_1.ConfigStorage();
|
|
59
83
|
const apiUrl = configStorage.getApiUrl();
|
|
60
|
-
const
|
|
84
|
+
const controlService = app.get(cli_control_cluster_service_1.CliControlClusterService);
|
|
61
85
|
const apiClient = new api_client_1.ApiClient({
|
|
62
86
|
baseUrl: apiUrl,
|
|
63
87
|
apiKey: configStorage.getApiKey(),
|
|
@@ -65,20 +89,6 @@ class EnvCreate extends core_1.Command {
|
|
|
65
89
|
const cacheService = new server_type_cache_service_1.ServerTypeCacheService();
|
|
66
90
|
const validatorService = new server_type_validator_service_1.ServerTypeValidatorService();
|
|
67
91
|
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
92
|
// 3. Resolve admin email (prompt if not set, then persist)
|
|
83
93
|
const emailResolver = new preferences_resolver_1.PreferencesResolver(configStorage);
|
|
84
94
|
const emailResult = emailResolver.resolve('email');
|
|
@@ -102,6 +112,7 @@ class EnvCreate extends core_1.Command {
|
|
|
102
112
|
// server-side, so the LE 5-certs-per-7-days rate limit never trips on
|
|
103
113
|
// repeated test creations.
|
|
104
114
|
const acmeStaging = flags['acme-staging'];
|
|
115
|
+
const useLatest = flags.latest;
|
|
105
116
|
spinner.stop();
|
|
106
117
|
if (acmeStaging) {
|
|
107
118
|
console.log(chalk_1.default.yellow(`\n⚠ ACME endpoint: Let's Encrypt STAGING — cert will not be browser-trusted (warning expected).\n`));
|
|
@@ -231,17 +242,17 @@ class EnvCreate extends core_1.Command {
|
|
|
231
242
|
spinner.succeed('Server type validation skipped (no data available)');
|
|
232
243
|
}
|
|
233
244
|
// Display cluster configuration
|
|
234
|
-
console.log(chalk_1.default.cyan('\n🚀 Creating Flui
|
|
245
|
+
console.log(chalk_1.default.cyan('\n🚀 Creating Flui Control Cluster (K3s)\n'));
|
|
235
246
|
console.log(chalk_1.default.dim(` Provider: ${providerKey}`));
|
|
236
247
|
console.log(chalk_1.default.dim(` Node Size: ${validatedNodeSize}`));
|
|
237
248
|
console.log(chalk_1.default.dim(` Region: ${validatedRegion}`));
|
|
238
249
|
console.log(chalk_1.default.dim(` Worker Nodes: ${flags['node-count']}\n`));
|
|
239
|
-
// 3. Check if
|
|
250
|
+
// 3. Check if control cluster already exists
|
|
240
251
|
spinner = (0, ora_1.default)('Checking for existing cluster...').start();
|
|
241
|
-
const existingCluster = await
|
|
252
|
+
const existingCluster = await controlService.getControlCluster();
|
|
242
253
|
if (existingCluster && existingCluster.status !== cluster_entity_1.ClusterStatus.DELETED) {
|
|
243
|
-
spinner.fail('
|
|
244
|
-
console.log(chalk_1.default.yellow('\n⚠️ An
|
|
254
|
+
spinner.fail('Control cluster already exists!');
|
|
255
|
+
console.log(chalk_1.default.yellow('\n⚠️ An control cluster is already running:\n'));
|
|
245
256
|
console.log(` ${chalk_1.default.bold('Name:')} ${existingCluster.name}`);
|
|
246
257
|
console.log(` ${chalk_1.default.bold('ID:')} ${existingCluster.id}`);
|
|
247
258
|
console.log(` ${chalk_1.default.bold('Status:')} ${existingCluster.status}`);
|
|
@@ -263,7 +274,7 @@ class EnvCreate extends core_1.Command {
|
|
|
263
274
|
const vnetService = app.get(vnet_provisioning_service_1.VnetProvisioningService);
|
|
264
275
|
envVnetInfo = await vnetService.ensureEnvVnet({
|
|
265
276
|
provider: cloudProvider,
|
|
266
|
-
name:
|
|
277
|
+
name: `flui-env-${(0, node_crypto_1.randomBytes)(3).toString('hex')}`,
|
|
267
278
|
ipRange: '10.10.0.0/16',
|
|
268
279
|
subnetIpRange: '10.10.1.0/24',
|
|
269
280
|
networkZone: cloudProvider === cloud_provider_enum_1.CloudProvider.HETZNER
|
|
@@ -277,7 +288,7 @@ class EnvCreate extends core_1.Command {
|
|
|
277
288
|
}
|
|
278
289
|
catch (error) {
|
|
279
290
|
spinner.fail(`VNet provisioning failed: ${error.message}`);
|
|
280
|
-
console.log(chalk_1.default.red('\n Cannot proceed without an environment VNet. The
|
|
291
|
+
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
292
|
this.exit(1);
|
|
282
293
|
}
|
|
283
294
|
// 4. Create firewall BEFORE cluster (if enabled)
|
|
@@ -296,15 +307,18 @@ class EnvCreate extends core_1.Command {
|
|
|
296
307
|
const publicIp = await ipService.getPublicIp();
|
|
297
308
|
sourceCidrs = [ipService.toCidr(publicIp)];
|
|
298
309
|
}
|
|
299
|
-
const rules = (0, firewall_rules_1.
|
|
300
|
-
|
|
310
|
+
const rules = (0, firewall_rules_1.CONTROL_FIREWALL_RULES)(sourceCidrs);
|
|
311
|
+
// Unique temporary name to avoid colliding with orphaned firewalls
|
|
312
|
+
// from previous runs; renamed to flui-control-firewall-<clusterId>
|
|
313
|
+
// (by firewallId) once the cluster is ready.
|
|
314
|
+
const temporaryFirewallName = `flui-control-firewall-${(0, node_crypto_1.randomBytes)(4).toString('hex')}`;
|
|
301
315
|
// Create firewall with temporary name (will be renamed when cluster is ready)
|
|
302
316
|
const result = await firewallService.createFirewall({
|
|
303
317
|
name: temporaryFirewallName,
|
|
304
318
|
labels: [
|
|
305
319
|
{ key: 'managed-by', value: 'flui-cloud' },
|
|
306
320
|
{ key: 'flui-resource-type', value: 'firewall' },
|
|
307
|
-
{ key: 'flui-cluster-type', value: '
|
|
321
|
+
{ key: 'flui-cluster-type', value: 'control' },
|
|
308
322
|
],
|
|
309
323
|
rules,
|
|
310
324
|
// Label selector will be applied later when cluster servers exist
|
|
@@ -326,10 +340,10 @@ class EnvCreate extends core_1.Command {
|
|
|
326
340
|
spinner.succeed(`Firewall created (Source: ${sourceCidrs.join(', ')})`);
|
|
327
341
|
}
|
|
328
342
|
catch (error) {
|
|
329
|
-
spinner.
|
|
330
|
-
console.log(chalk_1.default.
|
|
331
|
-
|
|
332
|
-
|
|
343
|
+
spinner.fail(`Firewall creation failed: ${error.message}`);
|
|
344
|
+
console.log(chalk_1.default.red('\n Aborting: the control cluster must not be provisioned without a firewall.'));
|
|
345
|
+
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.'));
|
|
346
|
+
this.exit(1);
|
|
333
347
|
}
|
|
334
348
|
}
|
|
335
349
|
// 5. Create K3s cluster
|
|
@@ -337,7 +351,7 @@ class EnvCreate extends core_1.Command {
|
|
|
337
351
|
console.log(chalk_1.default.dim(flags['node-count'] > 0
|
|
338
352
|
? ' This will create master node + worker nodes'
|
|
339
353
|
: ' This will create master node only'));
|
|
340
|
-
const clusterId = await
|
|
354
|
+
const clusterId = await controlService.createControlCluster(cloudProvider, validatedRegion, validatedNodeSize, flags['node-count'], firewallId, sourceCidrs, flags['auth-mode'], envVnetInfo
|
|
341
355
|
? {
|
|
342
356
|
vnetProviderResourceId: envVnetInfo.vnetProviderResourceId,
|
|
343
357
|
vnetIpRange: envVnetInfo.vnetIpRange,
|
|
@@ -349,6 +363,7 @@ class EnvCreate extends core_1.Command {
|
|
|
349
363
|
: undefined, adminEmail, acmeStaging, flags['disk-size'], {
|
|
350
364
|
sharedStorageEnabled: !flags['no-shared-storage'],
|
|
351
365
|
sharedStorageVolumeSizeGb: flags['shared-storage-size'],
|
|
366
|
+
useLatest,
|
|
352
367
|
});
|
|
353
368
|
spinner.succeed('Cluster creation started!');
|
|
354
369
|
if (firewallId && clusterId) {
|
|
@@ -369,7 +384,7 @@ class EnvCreate extends core_1.Command {
|
|
|
369
384
|
}
|
|
370
385
|
if (flags.detached) {
|
|
371
386
|
// DETACHED MODE: Exit immediately with monitoring instructions
|
|
372
|
-
console.log(chalk_1.default.green('\n✅
|
|
387
|
+
console.log(chalk_1.default.green('\n✅ Control Cluster Creation Started!\n'));
|
|
373
388
|
console.log(chalk_1.default.cyan('📋 Cluster Details:\n'));
|
|
374
389
|
console.log(` ${chalk_1.default.bold('Cluster ID:')} ${clusterId}`);
|
|
375
390
|
console.log(` ${chalk_1.default.bold('Provider:')} ${cloudProvider}`);
|
|
@@ -391,7 +406,7 @@ class EnvCreate extends core_1.Command {
|
|
|
391
406
|
// WAIT MODE: Wait for cluster to be ready
|
|
392
407
|
spinner = (0, ora_1.default)('Waiting for cluster to be ready...').start();
|
|
393
408
|
try {
|
|
394
|
-
await
|
|
409
|
+
await controlService.waitForClusterReady(clusterId, 600000);
|
|
395
410
|
spinner.succeed('Cluster is ready!');
|
|
396
411
|
}
|
|
397
412
|
catch (error) {
|
|
@@ -399,7 +414,7 @@ class EnvCreate extends core_1.Command {
|
|
|
399
414
|
console.log(chalk_1.default.red(`\n❌ Error: ${error.message}\n`));
|
|
400
415
|
this.exit(1);
|
|
401
416
|
}
|
|
402
|
-
console.log(chalk_1.default.green('\n✅
|
|
417
|
+
console.log(chalk_1.default.green('\n✅ Control Cluster Created Successfully!\n'));
|
|
403
418
|
console.log(chalk_1.default.cyan('📋 Cluster Details:\n'));
|
|
404
419
|
console.log(` ${chalk_1.default.bold('Cluster ID:')} ${clusterId}`);
|
|
405
420
|
console.log(` ${chalk_1.default.bold('Provider:')} ${cloudProvider}`);
|
|
@@ -428,7 +443,7 @@ class EnvCreate extends core_1.Command {
|
|
|
428
443
|
// This ensures reconciliation runs even if SSH setup fails
|
|
429
444
|
let shouldExit = false;
|
|
430
445
|
const onReconcile = async () => {
|
|
431
|
-
const cluster = await
|
|
446
|
+
const cluster = await controlService.getControlCluster();
|
|
432
447
|
const masterIp = cluster?.masterIpAddress;
|
|
433
448
|
const nipToken = cluster?.nipHostnameToken;
|
|
434
449
|
const baseDomain = (0, nip_base_domain_util_1.buildNipBaseDomain)(masterIp ?? '', nipToken);
|
|
@@ -440,13 +455,13 @@ class EnvCreate extends core_1.Command {
|
|
|
440
455
|
const interval = 15_000;
|
|
441
456
|
const deadline = Date.now() + maxWait;
|
|
442
457
|
while (Date.now() < deadline) {
|
|
443
|
-
const valid = await
|
|
458
|
+
const valid = await controlService.waitForValidTls(certUrl, interval, interval, acmeStaging);
|
|
444
459
|
if (valid)
|
|
445
460
|
break;
|
|
446
461
|
elapsed += interval;
|
|
447
462
|
console.log(chalk_1.default.dim(` Certificate pending... (${Math.round(elapsed / 1000)}s / ${maxWait / 1000}s)`));
|
|
448
463
|
}
|
|
449
|
-
const tlsReady = await
|
|
464
|
+
const tlsReady = await controlService.waitForValidTls(certUrl, 5_000, 5_000, acmeStaging);
|
|
450
465
|
if (tlsReady) {
|
|
451
466
|
console.log(chalk_1.default.green(acmeStaging
|
|
452
467
|
? "✅ TLS certificate valid (Let's Encrypt STAGING — not browser-trusted)"
|
|
@@ -477,12 +492,14 @@ class EnvCreate extends core_1.Command {
|
|
|
477
492
|
}
|
|
478
493
|
}
|
|
479
494
|
console.log(chalk_1.default.dim(`\n→ Waiting for OIDC client provisioning...`));
|
|
480
|
-
const oidcReady = await
|
|
495
|
+
const oidcReady = await controlService.waitForOidcReady(apiBaseUrl, 300_000, 10_000, acmeStaging);
|
|
481
496
|
if (oidcReady) {
|
|
482
497
|
console.log(chalk_1.default.green('✅ OIDC login ready'));
|
|
483
498
|
}
|
|
484
499
|
else {
|
|
485
|
-
console.log(chalk_1.default.yellow('⚠ OIDC not ready —
|
|
500
|
+
console.log(chalk_1.default.yellow('⚠ OIDC login not ready — the dashboard sign-in will fail until this is resolved.'));
|
|
501
|
+
console.log(chalk_1.default.dim(' Diagnose with `flui env status`, or `flui ssh master` + kubectl (check the zitadel and flui-web pods).\n' +
|
|
502
|
+
' `flui env force-ready` does NOT retry bootstrap — it only overrides the local status, so use it only after the cause is fixed.'));
|
|
486
503
|
}
|
|
487
504
|
}
|
|
488
505
|
}
|
|
@@ -507,12 +524,12 @@ class EnvCreate extends core_1.Command {
|
|
|
507
524
|
'\n'));
|
|
508
525
|
shouldExit = true;
|
|
509
526
|
};
|
|
510
|
-
pollerHandle =
|
|
527
|
+
pollerHandle = controlService.pollOperationUntilReady(clusterId, onReconcile, onFailed);
|
|
511
528
|
// Phase 1: Wait for server provisioning (master IP)
|
|
512
529
|
spinner = (0, ora_1.default)('Provisioning server...').start();
|
|
513
530
|
let masterIp;
|
|
514
531
|
try {
|
|
515
|
-
masterIp = await
|
|
532
|
+
masterIp = await controlService.waitForMasterIp(clusterId, 600000, 5000);
|
|
516
533
|
spinner.succeed(`Server provisioned (${masterIp})`);
|
|
517
534
|
}
|
|
518
535
|
catch (error) {
|
|
@@ -528,7 +545,7 @@ class EnvCreate extends core_1.Command {
|
|
|
528
545
|
// Phase 2: Wait for server boot (SSH port open)
|
|
529
546
|
spinner = (0, ora_1.default)('Server booting...').start();
|
|
530
547
|
try {
|
|
531
|
-
await
|
|
548
|
+
await controlService.waitForPortReady(masterIp, 22, 600000, 5000);
|
|
532
549
|
spinner.succeed('Server online');
|
|
533
550
|
}
|
|
534
551
|
catch (error) {
|
|
@@ -542,7 +559,7 @@ class EnvCreate extends core_1.Command {
|
|
|
542
559
|
// Phase 3: Wait for SSH authentication (CA enrollment via cloud-init)
|
|
543
560
|
spinner = (0, ora_1.default)('Waiting for SSH access (CA enrollment)...').start();
|
|
544
561
|
try {
|
|
545
|
-
await
|
|
562
|
+
await controlService.waitForSshAuth(() => sshService.sshExec(masterIp, 'echo ok'), 600000, 10000);
|
|
546
563
|
spinner.succeed('SSH access ready');
|
|
547
564
|
}
|
|
548
565
|
catch (error) {
|
|
@@ -611,7 +628,7 @@ class EnvCreate extends core_1.Command {
|
|
|
611
628
|
}
|
|
612
629
|
}
|
|
613
630
|
catch (error) {
|
|
614
|
-
spinner.fail('Failed to create
|
|
631
|
+
spinner.fail('Failed to create control cluster');
|
|
615
632
|
if (firewallId) {
|
|
616
633
|
try {
|
|
617
634
|
spinner = (0, ora_1.default)('Cleaning up orphaned firewall...').start();
|
|
@@ -653,7 +670,7 @@ class EnvCreate extends core_1.Command {
|
|
|
653
670
|
}
|
|
654
671
|
}
|
|
655
672
|
}
|
|
656
|
-
EnvCreate.description = 'Create
|
|
673
|
+
EnvCreate.description = 'Create control cluster infrastructure on K3s';
|
|
657
674
|
EnvCreate.examples = [
|
|
658
675
|
'<%= config.bin %> <%= command.id %>',
|
|
659
676
|
'<%= config.bin %> <%= command.id %> --node-size cx32',
|
|
@@ -664,8 +681,7 @@ EnvCreate.examples = [
|
|
|
664
681
|
EnvCreate.flags = {
|
|
665
682
|
provider: core_1.Flags.string({
|
|
666
683
|
char: 'p',
|
|
667
|
-
description:
|
|
668
|
-
default: 'hetzner',
|
|
684
|
+
description: "Cloud provider for the control cluster (default: the profile's configured provider)",
|
|
669
685
|
options: ['hetzner', 'scaleway'],
|
|
670
686
|
}),
|
|
671
687
|
'node-size': core_1.Flags.string({
|
|
@@ -696,6 +712,7 @@ EnvCreate.flags = {
|
|
|
696
712
|
'configure-firewall': core_1.Flags.boolean({
|
|
697
713
|
description: 'Automatically configure firewall after cluster creation',
|
|
698
714
|
default: true,
|
|
715
|
+
allowNo: true,
|
|
699
716
|
}),
|
|
700
717
|
'firewall-ip': core_1.Flags.string({
|
|
701
718
|
description: 'Source IP/CIDR for firewall (comma-separated, default: auto-detect)',
|
|
@@ -708,6 +725,10 @@ EnvCreate.flags = {
|
|
|
708
725
|
description: "Use Let's Encrypt staging endpoint (untrusted cert, no rate limits) — useful while iterating to avoid burning prod quota",
|
|
709
726
|
default: false,
|
|
710
727
|
}),
|
|
728
|
+
latest: core_1.Flags.boolean({
|
|
729
|
+
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.',
|
|
730
|
+
default: false,
|
|
731
|
+
}),
|
|
711
732
|
'no-shared-storage': core_1.Flags.boolean({
|
|
712
733
|
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
734
|
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;
|