@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
|
@@ -0,0 +1,544 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
19
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
20
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
21
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
22
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
23
|
+
};
|
|
24
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
25
|
+
var ownKeys = function(o) {
|
|
26
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
27
|
+
var ar = [];
|
|
28
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
29
|
+
return ar;
|
|
30
|
+
};
|
|
31
|
+
return ownKeys(o);
|
|
32
|
+
};
|
|
33
|
+
return function (mod) {
|
|
34
|
+
if (mod && mod.__esModule) return mod;
|
|
35
|
+
var result = {};
|
|
36
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
37
|
+
__setModuleDefault(result, mod);
|
|
38
|
+
return result;
|
|
39
|
+
};
|
|
40
|
+
})();
|
|
41
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
42
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
43
|
+
};
|
|
44
|
+
var CliControlClusterService_1;
|
|
45
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
46
|
+
exports.CliControlClusterService = void 0;
|
|
47
|
+
const common_1 = require("@nestjs/common");
|
|
48
|
+
const cluster_entity_1 = require("../../../src/modules/infrastructure/clusters/entities/cluster.entity");
|
|
49
|
+
const cli_cluster_repository_1 = require("../lib/repositories/cli-cluster.repository");
|
|
50
|
+
const release_config_1 = require("../../../src/config/release.config");
|
|
51
|
+
const nip_base_domain_util_1 = require("../lib/nip-base-domain.util");
|
|
52
|
+
const cli_node_repository_1 = require("../lib/repositories/cli-node.repository");
|
|
53
|
+
const cli_clusters_service_1 = require("./cli-clusters.service");
|
|
54
|
+
const cli_operation_repository_1 = require("../lib/repositories/cli-operation.repository");
|
|
55
|
+
const cli_ssh_service_1 = require("./cli-ssh.service");
|
|
56
|
+
const infrastructure_operations_entity_1 = require("../../../src/modules/infrastructure/servers/entities/infrastructure-operations.entity");
|
|
57
|
+
const net = __importStar(require("node:net"));
|
|
58
|
+
const https = __importStar(require("node:https"));
|
|
59
|
+
/**
|
|
60
|
+
* CLI Control Cluster Service
|
|
61
|
+
*
|
|
62
|
+
* Simplified version of ObservabilityClusterService for CLI usage.
|
|
63
|
+
* Uses file-based repositories instead of TypeORM.
|
|
64
|
+
*/
|
|
65
|
+
let CliControlClusterService = CliControlClusterService_1 = class CliControlClusterService {
|
|
66
|
+
constructor(clusterRepository, nodeRepository, clustersService, operationRepository, sshService) {
|
|
67
|
+
this.clusterRepository = clusterRepository;
|
|
68
|
+
this.nodeRepository = nodeRepository;
|
|
69
|
+
this.clustersService = clustersService;
|
|
70
|
+
this.operationRepository = operationRepository;
|
|
71
|
+
this.sshService = sshService;
|
|
72
|
+
this.logger = new common_1.Logger(CliControlClusterService_1.name);
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Get the control cluster
|
|
76
|
+
*/
|
|
77
|
+
async getControlCluster() {
|
|
78
|
+
const cluster = await this.clusterRepository.findOne({
|
|
79
|
+
where: {
|
|
80
|
+
metadata: { isObservabilityCluster: true },
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
if (!cluster) {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
// Manually load nodes from separate repository
|
|
87
|
+
const nodes = await this.nodeRepository.find({
|
|
88
|
+
where: { clusterId: cluster.id },
|
|
89
|
+
});
|
|
90
|
+
cluster.nodes = nodes;
|
|
91
|
+
return cluster;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Check if control cluster exists
|
|
95
|
+
*/
|
|
96
|
+
async hasControlCluster() {
|
|
97
|
+
const cluster = await this.getControlCluster();
|
|
98
|
+
return cluster !== null;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Get observability service endpoints
|
|
102
|
+
*/
|
|
103
|
+
async getObservabilityEndpoints(clusterId) {
|
|
104
|
+
const cluster = await this.clusterRepository.findOne({
|
|
105
|
+
where: { id: clusterId },
|
|
106
|
+
});
|
|
107
|
+
if (!cluster?.masterIpAddress) {
|
|
108
|
+
return {};
|
|
109
|
+
}
|
|
110
|
+
const ip = cluster.masterIpAddress;
|
|
111
|
+
const baseDomain = (0, nip_base_domain_util_1.buildNipBaseDomain)(ip, cluster.nipHostnameToken);
|
|
112
|
+
return {
|
|
113
|
+
prometheus: `cluster-internal (kubectl port-forward -n flui-system svc/prometheus 9090)`,
|
|
114
|
+
grafana: `cluster-internal (kubectl port-forward -n flui-system svc/grafana 3000)`,
|
|
115
|
+
loki: `cluster-internal (kubectl port-forward -n flui-system svc/loki 3100)`,
|
|
116
|
+
postgres: `cluster-internal (kubectl port-forward -n flui-system svc/postgres 5432)`,
|
|
117
|
+
redis: `cluster-internal (kubectl port-forward -n flui-system svc/redis 6379)`,
|
|
118
|
+
fluiApi: `https://api.${baseDomain}`,
|
|
119
|
+
fluiWeb: `https://app.${baseDomain}`,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Create control cluster
|
|
124
|
+
*/
|
|
125
|
+
async createControlCluster(provider, region, nodeSize, workerCount = 0, firewallId, sourceCidrs, authMode = 'local', envVnet, adminEmail, acmeStaging, diskSizeGb, options) {
|
|
126
|
+
// Pin the install to the CLI release (bootstrap ref + image tags), unless
|
|
127
|
+
// --latest opted into mobile dev tags. Recorded on the cluster so the
|
|
128
|
+
// installed version stays queryable after creation.
|
|
129
|
+
const useLatest = options?.useLatest ?? false;
|
|
130
|
+
const createDto = {
|
|
131
|
+
name: 'control-cluster',
|
|
132
|
+
provider: provider,
|
|
133
|
+
region,
|
|
134
|
+
nodeSize,
|
|
135
|
+
workerCount,
|
|
136
|
+
image: 'ubuntu-24.04',
|
|
137
|
+
diskSizeGb,
|
|
138
|
+
sharedStorageEnabled: options?.sharedStorageEnabled,
|
|
139
|
+
sharedStorageVolumeSizeGb: options?.sharedStorageVolumeSizeGb,
|
|
140
|
+
metadata: {
|
|
141
|
+
isObservabilityCluster: true,
|
|
142
|
+
firewallId,
|
|
143
|
+
sourceCidrs,
|
|
144
|
+
authMode,
|
|
145
|
+
envVnet,
|
|
146
|
+
adminEmail,
|
|
147
|
+
acmeStaging,
|
|
148
|
+
useLatest,
|
|
149
|
+
platformVersion: useLatest ? null : release_config_1.RELEASE.version,
|
|
150
|
+
componentVersions: (0, release_config_1.resolveImageTags)(useLatest),
|
|
151
|
+
bootstrapRef: (0, release_config_1.resolveBootstrapRef)(useLatest),
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
const { cluster } = await this.clustersService.create(createDto);
|
|
155
|
+
return cluster.id;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Deploy observability stack (Prometheus, Grafana, Loki)
|
|
159
|
+
*/
|
|
160
|
+
async deployObservabilityStack(clusterId) {
|
|
161
|
+
// TODO: Implement observability stack deployment
|
|
162
|
+
this.logger.warn('Observability stack deployment not implemented in CLI mode');
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Delete control cluster
|
|
166
|
+
*/
|
|
167
|
+
async deleteControlCluster() {
|
|
168
|
+
// Local clusters.json can accumulate stale observability entries (e.g. a
|
|
169
|
+
// previous destroy died mid-way, or a create crashed after persisting).
|
|
170
|
+
// findOne would only catch one — so we iterate and purge all matches.
|
|
171
|
+
const clusters = await this.clusterRepository.find({
|
|
172
|
+
where: { metadata: { isObservabilityCluster: true } },
|
|
173
|
+
});
|
|
174
|
+
if (clusters.length === 0) {
|
|
175
|
+
throw new Error('No control cluster found');
|
|
176
|
+
}
|
|
177
|
+
if (clusters.length > 1) {
|
|
178
|
+
this.logger.warn(`Found ${clusters.length} control cluster records — removing all to clear stale state`);
|
|
179
|
+
}
|
|
180
|
+
let lastError = null;
|
|
181
|
+
for (const cluster of clusters) {
|
|
182
|
+
try {
|
|
183
|
+
await this.clustersService.remove(cluster.id);
|
|
184
|
+
}
|
|
185
|
+
catch (error) {
|
|
186
|
+
lastError = error;
|
|
187
|
+
this.logger.error(`Failed to remove cluster ${cluster.id} (${cluster.name}): ${error.message}`);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
if (lastError)
|
|
191
|
+
throw lastError;
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Get cluster operation by cluster ID
|
|
195
|
+
*/
|
|
196
|
+
async getClusterOperation(clusterId) {
|
|
197
|
+
return this.operationRepository.findOne({
|
|
198
|
+
where: {
|
|
199
|
+
resourceId: clusterId,
|
|
200
|
+
resourceType: 'cluster',
|
|
201
|
+
},
|
|
202
|
+
order: {
|
|
203
|
+
createdAt: 'DESC',
|
|
204
|
+
},
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Wait for cluster to be ready by polling operation status
|
|
209
|
+
* @param clusterId Cluster ID to wait for
|
|
210
|
+
* @param timeoutMs Timeout in milliseconds (default: 10 minutes)
|
|
211
|
+
* @param pollIntervalMs Polling interval in milliseconds (default: 10 seconds)
|
|
212
|
+
* @returns Promise that resolves when cluster is ready
|
|
213
|
+
* @throws Error if timeout is reached or operation fails
|
|
214
|
+
*/
|
|
215
|
+
async waitForClusterReady(clusterId, timeoutMs = 600000, pollIntervalMs = 10000) {
|
|
216
|
+
const startTime = Date.now();
|
|
217
|
+
const maxAttempts = Math.ceil(timeoutMs / pollIntervalMs);
|
|
218
|
+
this.logger.log(`Waiting for cluster ${clusterId} to be ready (timeout: ${timeoutMs}ms, poll interval: ${pollIntervalMs}ms)`);
|
|
219
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
220
|
+
const operation = await this.getClusterOperation(clusterId);
|
|
221
|
+
if (!operation) {
|
|
222
|
+
throw new Error(`No operation found for cluster ${clusterId}. Cluster may not exist.`);
|
|
223
|
+
}
|
|
224
|
+
this.logger.debug(`Poll attempt ${attempt}/${maxAttempts}: Operation status = ${operation.status}, progress = ${operation.progress}%`);
|
|
225
|
+
// Check if operation completed successfully
|
|
226
|
+
if (operation.status === infrastructure_operations_entity_1.OperationStatus.COMPLETED) {
|
|
227
|
+
this.logger.log(`Cluster ${clusterId} is ready (took ${Date.now() - startTime}ms)`);
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
// Check if operation failed
|
|
231
|
+
if (operation.status === infrastructure_operations_entity_1.OperationStatus.FAILED) {
|
|
232
|
+
const errorMsg = operation.metadata?.error || 'Unknown error during cluster creation';
|
|
233
|
+
throw new Error(`Cluster creation failed: ${errorMsg}`);
|
|
234
|
+
}
|
|
235
|
+
// Check timeout
|
|
236
|
+
if (Date.now() - startTime >= timeoutMs) {
|
|
237
|
+
throw new Error(`Timeout waiting for cluster ${clusterId} to be ready after ${timeoutMs}ms. Current status: ${operation.status}, progress: ${operation.progress}%`);
|
|
238
|
+
}
|
|
239
|
+
// Wait before next poll (unless it's the last attempt)
|
|
240
|
+
if (attempt < maxAttempts) {
|
|
241
|
+
await this.sleep(pollIntervalMs);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
throw new Error(`Failed to confirm cluster readiness after ${maxAttempts} attempts`);
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Wait for master node IP address to become available
|
|
248
|
+
* Polls the cluster repository until masterIpAddress is populated
|
|
249
|
+
*/
|
|
250
|
+
async waitForMasterIp(clusterId, timeoutMs = 600000, pollIntervalMs = 5000) {
|
|
251
|
+
const startTime = Date.now();
|
|
252
|
+
this.logger.log(`Waiting for master IP of cluster ${clusterId} (timeout: ${timeoutMs}ms)`);
|
|
253
|
+
while (Date.now() - startTime < timeoutMs) {
|
|
254
|
+
const cluster = await this.clusterRepository.findOne({
|
|
255
|
+
where: { id: clusterId },
|
|
256
|
+
});
|
|
257
|
+
if (!cluster) {
|
|
258
|
+
throw new Error(`Cluster ${clusterId} not found. It may have been deleted.`);
|
|
259
|
+
}
|
|
260
|
+
if (cluster.status === cluster_entity_1.ClusterStatus.ERROR) {
|
|
261
|
+
throw new Error('Cluster creation failed before master IP was assigned.');
|
|
262
|
+
}
|
|
263
|
+
if (cluster.masterIpAddress) {
|
|
264
|
+
this.logger.log(`Master IP available: ${cluster.masterIpAddress}`);
|
|
265
|
+
return cluster.masterIpAddress;
|
|
266
|
+
}
|
|
267
|
+
await this.sleep(pollIntervalMs);
|
|
268
|
+
}
|
|
269
|
+
throw new Error(`Timeout waiting for master IP address after ${timeoutMs}ms`);
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Wait for TCP port to become reachable on a host
|
|
273
|
+
*/
|
|
274
|
+
async waitForPortReady(host, port = 22, timeoutMs = 600000, pollIntervalMs = 5000) {
|
|
275
|
+
const startTime = Date.now();
|
|
276
|
+
this.logger.log(`Waiting for port ${port} on ${host} (timeout: ${timeoutMs}ms)`);
|
|
277
|
+
while (Date.now() - startTime < timeoutMs) {
|
|
278
|
+
const isReachable = await this.checkTcpPort(host, port);
|
|
279
|
+
if (isReachable) {
|
|
280
|
+
this.logger.log(`Port ${port} is reachable on ${host}`);
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
await this.sleep(pollIntervalMs);
|
|
284
|
+
}
|
|
285
|
+
throw new Error(`Timeout waiting for port ${port} on ${host} after ${timeoutMs}ms`);
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Wait for SSH to be fully ready (CA enrolled + cert auth working)
|
|
289
|
+
* Attempts an actual SSH command with retry until it succeeds.
|
|
290
|
+
* This is needed because TCP port 22 can be open before cloud-init
|
|
291
|
+
* has finished configuring the CA for certificate authentication.
|
|
292
|
+
*
|
|
293
|
+
* @param sshTestFn Function that attempts an SSH command and throws on failure
|
|
294
|
+
*/
|
|
295
|
+
async waitForSshAuth(sshTestFn, timeoutMs = 600000, pollIntervalMs = 10000) {
|
|
296
|
+
const startTime = Date.now();
|
|
297
|
+
this.logger.log(`Waiting for SSH authentication to be ready (timeout: ${timeoutMs}ms)`);
|
|
298
|
+
let lastError;
|
|
299
|
+
while (Date.now() - startTime < timeoutMs) {
|
|
300
|
+
try {
|
|
301
|
+
await sshTestFn();
|
|
302
|
+
this.logger.log('SSH authentication is ready');
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
catch (error) {
|
|
306
|
+
lastError = error;
|
|
307
|
+
}
|
|
308
|
+
await this.sleep(pollIntervalMs);
|
|
309
|
+
}
|
|
310
|
+
if (lastError) {
|
|
311
|
+
this.logger.error(`SSH exec failed: ${lastError.message}`);
|
|
312
|
+
}
|
|
313
|
+
throw new Error(`Timeout waiting for SSH authentication after ${timeoutMs}ms`);
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Poll operation status in background and call onReady when COMPLETED.
|
|
317
|
+
* Returns a stop function and a done promise that resolves when the
|
|
318
|
+
* poller finishes (after onReady/onFailed callback completes or stop() is called).
|
|
319
|
+
*/
|
|
320
|
+
pollOperationUntilReady(clusterId, onReady, onFailed, pollIntervalMs = 10000) {
|
|
321
|
+
let stopped = false;
|
|
322
|
+
const poll = async () => {
|
|
323
|
+
while (!stopped) {
|
|
324
|
+
try {
|
|
325
|
+
const operation = await this.getClusterOperation(clusterId);
|
|
326
|
+
if (!operation) {
|
|
327
|
+
this.logger.warn(`No operation found for cluster ${clusterId} during background poll`);
|
|
328
|
+
await this.sleep(pollIntervalMs);
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
if (operation.status === infrastructure_operations_entity_1.OperationStatus.COMPLETED) {
|
|
332
|
+
this.logger.log(`Background poll: cluster ${clusterId} is READY`);
|
|
333
|
+
if (!stopped) {
|
|
334
|
+
await onReady();
|
|
335
|
+
}
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
if (operation.status === infrastructure_operations_entity_1.OperationStatus.FAILED) {
|
|
339
|
+
const errorMsg = operation.metadata?.error ||
|
|
340
|
+
'Unknown error during cluster creation';
|
|
341
|
+
this.logger.error(`Background poll: cluster creation failed: ${errorMsg}`);
|
|
342
|
+
if (onFailed && !stopped) {
|
|
343
|
+
onFailed(errorMsg);
|
|
344
|
+
}
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
catch (error) {
|
|
349
|
+
this.logger.debug(`Background poll error: ${error.message}`);
|
|
350
|
+
}
|
|
351
|
+
await this.sleep(pollIntervalMs);
|
|
352
|
+
}
|
|
353
|
+
};
|
|
354
|
+
const done = poll().catch((error) => {
|
|
355
|
+
this.logger.error(`Background poller crashed: ${error.message}`);
|
|
356
|
+
});
|
|
357
|
+
return {
|
|
358
|
+
stop: () => {
|
|
359
|
+
stopped = true;
|
|
360
|
+
},
|
|
361
|
+
done,
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* Check observability services health via HTTP endpoints
|
|
366
|
+
* @param masterIp Master node IP address
|
|
367
|
+
* @returns Object with service health status
|
|
368
|
+
*/
|
|
369
|
+
async checkObservabilityServices(masterIp, _nipHostnameToken) {
|
|
370
|
+
// Match by (unique) workload name only — the observability stack lives in
|
|
371
|
+
// `flui-control` on new installs and `flui-observability` on legacy ones.
|
|
372
|
+
const lookups = [
|
|
373
|
+
{ ns: 'control', name: 'vmsingle', key: 'prometheus' },
|
|
374
|
+
{ ns: 'control', name: 'grafana', key: 'grafana' },
|
|
375
|
+
{ ns: 'control', name: 'loki', key: 'loki' },
|
|
376
|
+
{ ns: 'flui-system', name: 'postgres', key: 'postgres' },
|
|
377
|
+
{ ns: 'flui-system', name: 'redis', key: 'redis' },
|
|
378
|
+
{ ns: 'flui-system', name: 'flui-api', key: 'fluiApi' },
|
|
379
|
+
{ ns: 'flui-system', name: 'flui-web', key: 'fluiWeb' },
|
|
380
|
+
];
|
|
381
|
+
const result = {
|
|
382
|
+
prometheus: 'unreachable',
|
|
383
|
+
grafana: 'unreachable',
|
|
384
|
+
loki: 'unreachable',
|
|
385
|
+
postgres: 'unreachable',
|
|
386
|
+
redis: 'unreachable',
|
|
387
|
+
fluiApi: 'unreachable',
|
|
388
|
+
fluiWeb: 'unreachable',
|
|
389
|
+
};
|
|
390
|
+
try {
|
|
391
|
+
const combined = `(kubectl -n flui-control get deploy,statefulset -o json 2>/dev/null || echo '{"items":[]}') ; ` +
|
|
392
|
+
`echo '---FLUI-SEP---' ; ` +
|
|
393
|
+
`(kubectl -n flui-observability get deploy,statefulset -o json 2>/dev/null || echo '{"items":[]}') ; ` +
|
|
394
|
+
`echo '---FLUI-SEP---' ; ` +
|
|
395
|
+
`(kubectl -n flui-system get deploy,statefulset -o json 2>/dev/null || echo '{"items":[]}')`;
|
|
396
|
+
const raw = await this.sshService.sshExec(masterIp, combined);
|
|
397
|
+
const [controlRaw, legacyRaw, sysRaw] = raw.split('---FLUI-SEP---');
|
|
398
|
+
const collect = (json) => {
|
|
399
|
+
try {
|
|
400
|
+
const parsed = JSON.parse(json);
|
|
401
|
+
return parsed.items ?? [];
|
|
402
|
+
}
|
|
403
|
+
catch {
|
|
404
|
+
return [];
|
|
405
|
+
}
|
|
406
|
+
};
|
|
407
|
+
const items = [
|
|
408
|
+
...collect(controlRaw || ''),
|
|
409
|
+
...collect(legacyRaw || ''),
|
|
410
|
+
...collect(sysRaw || ''),
|
|
411
|
+
];
|
|
412
|
+
for (const lookup of lookups) {
|
|
413
|
+
const item = items.find((i) => lookup.ns === 'control'
|
|
414
|
+
? i.metadata?.name === lookup.name
|
|
415
|
+
: i.metadata?.name === lookup.name &&
|
|
416
|
+
i.metadata?.namespace === lookup.ns);
|
|
417
|
+
const replicas = item?.status?.replicas ?? 0;
|
|
418
|
+
const ready = item?.status?.readyReplicas ?? 0;
|
|
419
|
+
result[lookup.key] =
|
|
420
|
+
replicas > 0 && ready === replicas ? 'healthy' : 'unreachable';
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
catch {
|
|
424
|
+
// On SSH failure, leave defaults (all unreachable).
|
|
425
|
+
}
|
|
426
|
+
return result;
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* Check if a TCP port is reachable on a host
|
|
430
|
+
*/
|
|
431
|
+
checkTcpPort(host, port, timeout = 5000) {
|
|
432
|
+
return new Promise((resolve) => {
|
|
433
|
+
const socket = new net.Socket();
|
|
434
|
+
const timer = setTimeout(() => {
|
|
435
|
+
socket.destroy();
|
|
436
|
+
resolve(false);
|
|
437
|
+
}, timeout);
|
|
438
|
+
socket.on('connect', () => {
|
|
439
|
+
clearTimeout(timer);
|
|
440
|
+
socket.destroy();
|
|
441
|
+
resolve(true);
|
|
442
|
+
});
|
|
443
|
+
socket.on('error', () => {
|
|
444
|
+
clearTimeout(timer);
|
|
445
|
+
resolve(false);
|
|
446
|
+
});
|
|
447
|
+
socket.connect(port, host);
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
/**
|
|
451
|
+
* Sleep helper
|
|
452
|
+
*/
|
|
453
|
+
sleep(ms) {
|
|
454
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
455
|
+
}
|
|
456
|
+
async waitForValidTls(url, timeoutMs = 300_000, intervalMs = 15_000, acmeStaging = false) {
|
|
457
|
+
// Production: rely on Node's default chain validation (LE prod root is in the OS trust store).
|
|
458
|
+
// Staging: chain is signed by LE staging fake CA (not in OS trust store), so we connect with
|
|
459
|
+
// rejectUnauthorized=false but then inspect the served cert issuer to ensure it's a real LE
|
|
460
|
+
// cert and not Traefik's self-signed default.
|
|
461
|
+
const agent = acmeStaging
|
|
462
|
+
? new https.Agent({ rejectUnauthorized: false })
|
|
463
|
+
: undefined;
|
|
464
|
+
const deadline = Date.now() + timeoutMs;
|
|
465
|
+
while (Date.now() < deadline) {
|
|
466
|
+
const valid = await new Promise((resolve) => {
|
|
467
|
+
const req = https.request(url, { method: 'HEAD', agent }, (res) => {
|
|
468
|
+
if (!acmeStaging) {
|
|
469
|
+
resolve(true);
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
const cert = res.socket.getPeerCertificate?.();
|
|
473
|
+
const issuerO = cert?.issuer?.O ?? '';
|
|
474
|
+
resolve(issuerO.includes("Let's Encrypt"));
|
|
475
|
+
});
|
|
476
|
+
req.on('error', () => resolve(false));
|
|
477
|
+
req.setTimeout(10_000, () => {
|
|
478
|
+
req.destroy();
|
|
479
|
+
resolve(false);
|
|
480
|
+
});
|
|
481
|
+
req.end();
|
|
482
|
+
});
|
|
483
|
+
if (valid)
|
|
484
|
+
return true;
|
|
485
|
+
await this.sleep(intervalMs);
|
|
486
|
+
}
|
|
487
|
+
return false;
|
|
488
|
+
}
|
|
489
|
+
async waitForOidcReady(apiBaseUrl, timeoutMs = 300_000, intervalMs = 10_000, acmeStaging = false) {
|
|
490
|
+
const url = `${apiBaseUrl.replace(/\/$/, '')}/api/v1/health/oidc`;
|
|
491
|
+
const agent = acmeStaging
|
|
492
|
+
? new https.Agent({ rejectUnauthorized: false })
|
|
493
|
+
: undefined;
|
|
494
|
+
const deadline = Date.now() + timeoutMs;
|
|
495
|
+
while (Date.now() < deadline) {
|
|
496
|
+
const ready = await new Promise((resolve) => {
|
|
497
|
+
const req = https.get(url, { timeout: 10_000, agent }, (res) => {
|
|
498
|
+
if (acmeStaging) {
|
|
499
|
+
const cert = res.socket.getPeerCertificate?.();
|
|
500
|
+
const issuerO = cert?.issuer?.O ?? '';
|
|
501
|
+
if (!issuerO.includes("Let's Encrypt")) {
|
|
502
|
+
res.resume();
|
|
503
|
+
resolve(false);
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
if (res.statusCode !== 200) {
|
|
508
|
+
res.resume();
|
|
509
|
+
resolve(false);
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
let body = '';
|
|
513
|
+
res.on('data', (chunk) => (body += chunk));
|
|
514
|
+
res.on('end', () => {
|
|
515
|
+
try {
|
|
516
|
+
resolve(!!JSON.parse(body).ready);
|
|
517
|
+
}
|
|
518
|
+
catch {
|
|
519
|
+
resolve(false);
|
|
520
|
+
}
|
|
521
|
+
});
|
|
522
|
+
});
|
|
523
|
+
req.on('error', () => resolve(false));
|
|
524
|
+
req.on('timeout', () => {
|
|
525
|
+
req.destroy();
|
|
526
|
+
resolve(false);
|
|
527
|
+
});
|
|
528
|
+
});
|
|
529
|
+
if (ready)
|
|
530
|
+
return true;
|
|
531
|
+
await this.sleep(intervalMs);
|
|
532
|
+
}
|
|
533
|
+
return false;
|
|
534
|
+
}
|
|
535
|
+
};
|
|
536
|
+
exports.CliControlClusterService = CliControlClusterService;
|
|
537
|
+
exports.CliControlClusterService = CliControlClusterService = CliControlClusterService_1 = __decorate([
|
|
538
|
+
(0, common_1.Injectable)(),
|
|
539
|
+
__metadata("design:paramtypes", [cli_cluster_repository_1.CliClusterRepository,
|
|
540
|
+
cli_node_repository_1.CliNodeRepository,
|
|
541
|
+
cli_clusters_service_1.CliClustersService,
|
|
542
|
+
cli_operation_repository_1.CliOperationRepository,
|
|
543
|
+
cli_ssh_service_1.CliSshService])
|
|
544
|
+
], CliControlClusterService);
|
|
@@ -19,6 +19,7 @@ export interface SystemEndpoints {
|
|
|
19
19
|
oidcAudience: string;
|
|
20
20
|
/** Public OIDC client ID consumed by the dashboard (flui-web-config ConfigMap). */
|
|
21
21
|
oidcClientId: string;
|
|
22
|
+
oidcCliClientId: string;
|
|
22
23
|
}
|
|
23
24
|
export declare class CliEndpointResolverService {
|
|
24
25
|
private readonly sshService;
|
|
@@ -64,6 +64,7 @@ let CliEndpointResolverService = CliEndpointResolverService_1 = class CliEndpoin
|
|
|
64
64
|
const authMode = configMapData['AUTH_MODE'] ?? 'unknown';
|
|
65
65
|
const oidcIssuer = configMapData['OIDC_ISSUER'] ?? '';
|
|
66
66
|
const oidcJwksUri = configMapData['OIDC_JWKS_URI'] ?? '';
|
|
67
|
+
const oidcCliClientId = configMapData['OIDC_CLI_CLIENT_ID'] ?? '';
|
|
67
68
|
const oidcAudience = secretData['OIDC_AUDIENCE']
|
|
68
69
|
? Buffer.from(secretData['OIDC_AUDIENCE'], 'base64').toString('utf-8')
|
|
69
70
|
: '';
|
|
@@ -93,6 +94,7 @@ let CliEndpointResolverService = CliEndpointResolverService_1 = class CliEndpoin
|
|
|
93
94
|
oidcJwksUri,
|
|
94
95
|
oidcAudience,
|
|
95
96
|
oidcClientId,
|
|
97
|
+
oidcCliClientId,
|
|
96
98
|
};
|
|
97
99
|
}
|
|
98
100
|
async fetchSnapshot(masterIp) {
|
|
@@ -114,8 +116,12 @@ let CliEndpointResolverService = CliEndpointResolverService_1 = class CliEndpoin
|
|
|
114
116
|
}
|
|
115
117
|
}
|
|
116
118
|
resolveApp(key, spec, ingresses, ingressRoutes, masterIp, nipHostnameToken) {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
+
// Prefer the k8s native Ingress over Traefik IngressRoute when both exist:
|
|
120
|
+
// `configure-system-ingress` writes Ingress with the user-chosen domain
|
|
121
|
+
// (e.g. *.flui.cloud), while the legacy IngressRoute may still carry
|
|
122
|
+
// the nip.io hostname seeded at cluster bootstrap.
|
|
123
|
+
const match = this.findIngressMatch(spec, ingresses) ??
|
|
124
|
+
this.findIngressRouteMatch(spec, ingressRoutes);
|
|
119
125
|
const baseDomain = (0, nip_base_domain_util_1.buildNipBaseDomain)(masterIp, nipHostnameToken);
|
|
120
126
|
const defaultFqdn = spec.defaultSubdomain
|
|
121
127
|
? `${spec.defaultSubdomain}.${baseDomain}`
|
|
@@ -35,6 +35,11 @@ export interface K3sMasterConfig {
|
|
|
35
35
|
clusterFirewallId?: string;
|
|
36
36
|
nipIoCertEnabled?: boolean;
|
|
37
37
|
acmeStaging?: boolean;
|
|
38
|
+
/**
|
|
39
|
+
* Install from mobile tags instead of the pinned release: bootstrap ref
|
|
40
|
+
* `master` + `:latest` Docker images. Set by `flui env create --latest`.
|
|
41
|
+
*/
|
|
42
|
+
useLatest?: boolean;
|
|
38
43
|
nipHostnameToken?: string | null;
|
|
39
44
|
envVnet?: {
|
|
40
45
|
vnetProviderResourceId: string;
|
|
@@ -70,6 +75,8 @@ export interface K3sWorkerConfig {
|
|
|
70
75
|
provider: string;
|
|
71
76
|
caPublicKey?: string;
|
|
72
77
|
operationId?: string;
|
|
78
|
+
/** See K3sMasterConfig.useLatest — keeps the worker on the same bootstrap ref. */
|
|
79
|
+
useLatest?: boolean;
|
|
73
80
|
/**
|
|
74
81
|
* Flui shared storage on workers: install cachefilesd + mount NFS export
|
|
75
82
|
* from master. See scaling doc §14.
|
|
@@ -82,7 +89,7 @@ export interface K3sWorkerConfig {
|
|
|
82
89
|
/**
|
|
83
90
|
* CLI K3s Script Service
|
|
84
91
|
*
|
|
85
|
-
* Generates K3s initialization scripts for
|
|
92
|
+
* Generates K3s initialization scripts for control clusters.
|
|
86
93
|
* Uses scripts from cli/src/modules/instances/assets/scripts/ directory.
|
|
87
94
|
*/
|
|
88
95
|
export declare class CliK3sScriptService {
|
|
@@ -50,10 +50,11 @@ const path = __importStar(require("node:path"));
|
|
|
50
50
|
const os = __importStar(require("node:os"));
|
|
51
51
|
const cli_logger_service_1 = require("./cli-logger.service");
|
|
52
52
|
const bootstrap_config_1 = require("../config/bootstrap.config");
|
|
53
|
+
const release_config_1 = require("../../../src/config/release.config");
|
|
53
54
|
/**
|
|
54
55
|
* CLI K3s Script Service
|
|
55
56
|
*
|
|
56
|
-
* Generates K3s initialization scripts for
|
|
57
|
+
* Generates K3s initialization scripts for control clusters.
|
|
57
58
|
* Uses scripts from cli/src/modules/instances/assets/scripts/ directory.
|
|
58
59
|
*/
|
|
59
60
|
let CliK3sScriptService = CliK3sScriptService_1 = class CliK3sScriptService {
|
|
@@ -93,11 +94,13 @@ let CliK3sScriptService = CliK3sScriptService_1 = class CliK3sScriptService {
|
|
|
93
94
|
try {
|
|
94
95
|
this.log(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`, opId);
|
|
95
96
|
this.log(`[BOOTSTRAP MASTER SCRIPT] Cluster: ${config.clusterName}`, opId);
|
|
96
|
-
|
|
97
|
+
const scriptsBaseUrl = (0, bootstrap_config_1.getScriptsBaseUrl)(config.useLatest ?? false);
|
|
98
|
+
const imageTags = (0, release_config_1.resolveImageTags)(config.useLatest ?? false);
|
|
99
|
+
this.log(`Scripts URL: ${scriptsBaseUrl}`, opId);
|
|
97
100
|
// Generate bootstrap script that downloads and executes k3s-master-init.sh from GitHub
|
|
98
101
|
const script = this.generateBootstrapScript('master', {
|
|
99
|
-
SCRIPTS_BASE_URL:
|
|
100
|
-
MANIFESTS_BASE_URL:
|
|
102
|
+
SCRIPTS_BASE_URL: scriptsBaseUrl,
|
|
103
|
+
MANIFESTS_BASE_URL: scriptsBaseUrl.replace('/scripts', '/manifests'),
|
|
101
104
|
SERVER_ID: config.serverId || '', // Database node ID for observability
|
|
102
105
|
INSTANCE_ID: config.instanceId,
|
|
103
106
|
INSTANCE_NAME: config.instanceName,
|
|
@@ -106,6 +109,10 @@ let CliK3sScriptService = CliK3sScriptService_1 = class CliK3sScriptService {
|
|
|
106
109
|
CLUSTER_NAME: config.clusterName,
|
|
107
110
|
K3S_TOKEN: config.k3sToken,
|
|
108
111
|
K3S_VERSION: config.k3sVersion || 'v1.35.4+k3s1',
|
|
112
|
+
// Pinned Flui image tags — consumed by the system manifests via envsubst.
|
|
113
|
+
FLUI_API_IMAGE_TAG: imageTags.fluiApi,
|
|
114
|
+
FLUI_WEB_IMAGE_TAG: imageTags.fluiWeb,
|
|
115
|
+
FLUI_AUTHZ_IMAGE_TAG: imageTags.fluiAuthz,
|
|
109
116
|
DEPLOY_OBSERVABILITY_STACK: config.deployObservabilityStack
|
|
110
117
|
? 'true'
|
|
111
118
|
: 'false',
|
|
@@ -179,11 +186,12 @@ let CliK3sScriptService = CliK3sScriptService_1 = class CliK3sScriptService {
|
|
|
179
186
|
try {
|
|
180
187
|
this.log(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`, opId);
|
|
181
188
|
this.log(`[BOOTSTRAP WORKER SCRIPT] Cluster: ${config.clusterName}`, opId);
|
|
182
|
-
|
|
189
|
+
const scriptsBaseUrl = (0, bootstrap_config_1.getScriptsBaseUrl)(config.useLatest ?? false);
|
|
190
|
+
this.log(`Scripts URL: ${scriptsBaseUrl}`, opId);
|
|
183
191
|
// Generate bootstrap script that downloads and executes k3s-worker-init.sh from GitHub
|
|
184
192
|
const script = this.generateBootstrapScript('worker', {
|
|
185
193
|
SERVER_ID: config.serverId || '', // Database node ID for observability
|
|
186
|
-
SCRIPTS_BASE_URL:
|
|
194
|
+
SCRIPTS_BASE_URL: scriptsBaseUrl,
|
|
187
195
|
INSTANCE_ID: config.instanceId,
|
|
188
196
|
INSTANCE_NAME: config.instanceName,
|
|
189
197
|
CLOUD_PROVIDER: config.provider,
|