@quatrain/cli 1.1.8
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/README.md +60 -0
- package/bin/core.js +3 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +33 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/deploy.d.ts +2 -0
- package/dist/commands/deploy.d.ts.map +1 -0
- package/dist/commands/deploy.js +696 -0
- package/dist/commands/deploy.js.map +1 -0
- package/dist/commands/generate/config.d.ts +2 -0
- package/dist/commands/generate/config.d.ts.map +1 -0
- package/dist/commands/generate/config.js +121 -0
- package/dist/commands/generate/config.js.map +1 -0
- package/dist/commands/generate/migration.d.ts +2 -0
- package/dist/commands/generate/migration.d.ts.map +1 -0
- package/dist/commands/generate/migration.js +42 -0
- package/dist/commands/generate/migration.js.map +1 -0
- package/dist/commands/generate/scaffold.d.ts +2 -0
- package/dist/commands/generate/scaffold.d.ts.map +1 -0
- package/dist/commands/generate/scaffold.js +65 -0
- package/dist/commands/generate/scaffold.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +49 -0
- package/dist/index.js.map +1 -0
- package/package.json +42 -0
- package/src/cli.ts +38 -0
- package/src/commands/deploy.ts +829 -0
- package/src/commands/generate/config.ts +130 -0
- package/src/commands/generate/migration.ts +49 -0
- package/src/commands/generate/scaffold.ts +86 -0
- package/src/index.ts +59 -0
|
@@ -0,0 +1,696 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.deployCommand = deployCommand;
|
|
7
|
+
const inquirer_1 = __importDefault(require("inquirer"));
|
|
8
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
9
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
10
|
+
const node_child_process_1 = require("node:child_process");
|
|
11
|
+
function discoverCoreDeployPath() {
|
|
12
|
+
const cwd = process.cwd();
|
|
13
|
+
if (node_fs_1.default.existsSync(node_path_1.default.join(cwd, 'k8s/templates/namespace.yaml'))) {
|
|
14
|
+
return cwd;
|
|
15
|
+
}
|
|
16
|
+
const siblingCandidates = [
|
|
17
|
+
node_path_1.default.resolve(cwd, '../CoreDeploy'),
|
|
18
|
+
node_path_1.default.resolve(cwd, '../../CoreDeploy'),
|
|
19
|
+
node_path_1.default.resolve(cwd, 'CoreDeploy'),
|
|
20
|
+
'/Users/crapougnax/CODE/QUATRAIN/CoreDeploy'
|
|
21
|
+
];
|
|
22
|
+
for (const candidate of siblingCandidates) {
|
|
23
|
+
if (node_fs_1.default.existsSync(node_path_1.default.join(candidate, 'k8s/templates/namespace.yaml'))) {
|
|
24
|
+
return candidate;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return '';
|
|
28
|
+
}
|
|
29
|
+
function generateSecurePassword() {
|
|
30
|
+
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#%^*()-_=+[]{}';
|
|
31
|
+
let pass = '';
|
|
32
|
+
for (let i = 0; i < 24; i++) {
|
|
33
|
+
const idx = Math.floor(Math.random() * chars.length);
|
|
34
|
+
pass += chars[idx];
|
|
35
|
+
}
|
|
36
|
+
return pass;
|
|
37
|
+
}
|
|
38
|
+
function detectLatestStableTag(coreDeployPath) {
|
|
39
|
+
const fallback = '1.1.49';
|
|
40
|
+
try {
|
|
41
|
+
const packageJsonPath = node_path_1.default.resolve(coreDeployPath, '../CoreApps/containers/studio-image/package.json');
|
|
42
|
+
if (node_fs_1.default.existsSync(packageJsonPath)) {
|
|
43
|
+
const content = JSON.parse(node_fs_1.default.readFileSync(packageJsonPath, 'utf8'));
|
|
44
|
+
if (content && content.version) {
|
|
45
|
+
return content.version;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
}
|
|
51
|
+
return fallback;
|
|
52
|
+
}
|
|
53
|
+
function parseMetadataFromYaml(appDir, namespaceName) {
|
|
54
|
+
try {
|
|
55
|
+
const nsPath = node_path_1.default.join(appDir, 'namespace.yaml');
|
|
56
|
+
const ingPath = node_path_1.default.join(appDir, 'ingressroute.yaml');
|
|
57
|
+
const depPath = node_path_1.default.join(appDir, 'deployment.yaml');
|
|
58
|
+
const secPath = node_path_1.default.join(appDir, 'secret.yaml');
|
|
59
|
+
const pvcPath = node_path_1.default.join(appDir, 'pvc.yaml');
|
|
60
|
+
if (!node_fs_1.default.existsSync(nsPath) || !node_fs_1.default.existsSync(ingPath) || !node_fs_1.default.existsSync(depPath) || !node_fs_1.default.existsSync(secPath)) {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
const nsContent = node_fs_1.default.readFileSync(nsPath, 'utf8');
|
|
64
|
+
const ingContent = node_fs_1.default.readFileSync(ingPath, 'utf8');
|
|
65
|
+
const depContent = node_fs_1.default.readFileSync(depPath, 'utf8');
|
|
66
|
+
const secContent = node_fs_1.default.readFileSync(secPath, 'utf8');
|
|
67
|
+
const env = namespaceName.endsWith('-dev') ? 'dev' : 'prod';
|
|
68
|
+
let appName = namespaceName.replace(/-[a-z0-9]{5}(-dev)?$/, '');
|
|
69
|
+
const instanceMatch = nsContent.match(/app\.kubernetes\.io\/instance:\s*([^\s\n]+)/);
|
|
70
|
+
if (instanceMatch) {
|
|
71
|
+
appName = instanceMatch[1];
|
|
72
|
+
}
|
|
73
|
+
let domain = '';
|
|
74
|
+
const hostMatch = ingContent.match(/Host\(`([^`]+)`\)/);
|
|
75
|
+
if (hostMatch) {
|
|
76
|
+
domain = hostMatch[1];
|
|
77
|
+
}
|
|
78
|
+
let imageRef = '';
|
|
79
|
+
const imageMatch = depContent.match(/image:\s*(ghcr\.io\/quatrain\/studio-image:[^\s\n]+)/);
|
|
80
|
+
if (imageMatch) {
|
|
81
|
+
imageRef = imageMatch[1];
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
const generalImageMatch = depContent.match(/image:\s*([^\s\n]+)/);
|
|
85
|
+
if (generalImageMatch) {
|
|
86
|
+
imageRef = generalImageMatch[1];
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
let authUser = 'admin';
|
|
90
|
+
let authPass = '';
|
|
91
|
+
const userMatch = secContent.match(/STUDIO_AUTH_USER:\s*([^\s\n]+)/);
|
|
92
|
+
const passMatch = secContent.match(/STUDIO_AUTH_PASS:\s*([^\s\n]+)/);
|
|
93
|
+
if (userMatch) {
|
|
94
|
+
authUser = Buffer.from(userMatch[1].trim(), 'base64').toString('utf8');
|
|
95
|
+
}
|
|
96
|
+
if (passMatch) {
|
|
97
|
+
authPass = Buffer.from(passMatch[1].trim(), 'base64').toString('utf8');
|
|
98
|
+
}
|
|
99
|
+
let cpuRequests = '100m';
|
|
100
|
+
let cpuLimits = '500m';
|
|
101
|
+
let memRequests = '256Mi';
|
|
102
|
+
let memLimits = '512Mi';
|
|
103
|
+
const containerSplit = depContent.split('containers:');
|
|
104
|
+
if (containerSplit.length > 1) {
|
|
105
|
+
const resourcesPart = containerSplit[1];
|
|
106
|
+
const reqCpuMatch = resourcesPart.match(/requests:[\s\S]*?cpu:\s*"?([^\s\n"]+)"?/);
|
|
107
|
+
const reqMemMatch = resourcesPart.match(/requests:[\s\S]*?memory:\s*"?([^\s\n"]+)"?/);
|
|
108
|
+
const limCpuMatch = resourcesPart.match(/limits:[\s\S]*?cpu:\s*"?([^\s\n"]+)"?/);
|
|
109
|
+
const limMemMatch = resourcesPart.match(/limits:[\s\S]*?memory:\s*"?([^\s\n"]+)"?/);
|
|
110
|
+
if (reqCpuMatch)
|
|
111
|
+
cpuRequests = reqCpuMatch[1];
|
|
112
|
+
if (reqMemMatch)
|
|
113
|
+
memRequests = reqMemMatch[1];
|
|
114
|
+
if (limCpuMatch)
|
|
115
|
+
cpuLimits = limCpuMatch[1];
|
|
116
|
+
if (limMemMatch)
|
|
117
|
+
memLimits = limMemMatch[1];
|
|
118
|
+
}
|
|
119
|
+
let dataPvcSize = '1Gi';
|
|
120
|
+
let storagePvcSize = '10Gi';
|
|
121
|
+
if (node_fs_1.default.existsSync(pvcPath)) {
|
|
122
|
+
const pvcContent = node_fs_1.default.readFileSync(pvcPath, 'utf8');
|
|
123
|
+
const docs = pvcContent.split('---');
|
|
124
|
+
docs.forEach(doc => {
|
|
125
|
+
const nameMatch = doc.match(/name:\s*([^\s\n]+)/);
|
|
126
|
+
const storageMatch = doc.match(/storage:\s*([^\s\n]+)/);
|
|
127
|
+
if (nameMatch && storageMatch) {
|
|
128
|
+
if (nameMatch[1].includes('data-pvc')) {
|
|
129
|
+
dataPvcSize = storageMatch[1];
|
|
130
|
+
}
|
|
131
|
+
else if (nameMatch[1].includes('storage-pvc')) {
|
|
132
|
+
storagePvcSize = storageMatch[1];
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
const meta = {
|
|
138
|
+
appName,
|
|
139
|
+
namespace: namespaceName,
|
|
140
|
+
env,
|
|
141
|
+
domain,
|
|
142
|
+
imageRef,
|
|
143
|
+
authUser,
|
|
144
|
+
authPass,
|
|
145
|
+
resources: {
|
|
146
|
+
cpuRequests,
|
|
147
|
+
cpuLimits,
|
|
148
|
+
memRequests,
|
|
149
|
+
memLimits,
|
|
150
|
+
dataPvcSize,
|
|
151
|
+
storagePvcSize
|
|
152
|
+
},
|
|
153
|
+
createdAt: new Date().toISOString()
|
|
154
|
+
};
|
|
155
|
+
node_fs_1.default.writeFileSync(node_path_1.default.join(appDir, 'metadata.json'), JSON.stringify(meta, null, 3), 'utf8');
|
|
156
|
+
return meta;
|
|
157
|
+
}
|
|
158
|
+
catch (err) {
|
|
159
|
+
console.error(`Failed to parse YAML configuration for ${namespaceName}:`, err);
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
function loadAllDeployments(coreDeployPath) {
|
|
164
|
+
const appsDir = node_path_1.default.join(coreDeployPath, 'k8s/apps');
|
|
165
|
+
if (!node_fs_1.default.existsSync(appsDir)) {
|
|
166
|
+
return [];
|
|
167
|
+
}
|
|
168
|
+
const directories = node_fs_1.default.readdirSync(appsDir).filter(name => {
|
|
169
|
+
return node_fs_1.default.statSync(node_path_1.default.join(appsDir, name)).isDirectory();
|
|
170
|
+
});
|
|
171
|
+
const deployments = [];
|
|
172
|
+
for (const dir of directories) {
|
|
173
|
+
const appDir = node_path_1.default.join(appsDir, dir);
|
|
174
|
+
const metaPath = node_path_1.default.join(appDir, 'metadata.json');
|
|
175
|
+
if (node_fs_1.default.existsSync(metaPath)) {
|
|
176
|
+
try {
|
|
177
|
+
const data = JSON.parse(node_fs_1.default.readFileSync(metaPath, 'utf8'));
|
|
178
|
+
deployments.push(data);
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
const meta = parseMetadataFromYaml(appDir, dir);
|
|
182
|
+
if (meta)
|
|
183
|
+
deployments.push(meta);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
const meta = parseMetadataFromYaml(appDir, dir);
|
|
188
|
+
if (meta)
|
|
189
|
+
deployments.push(meta);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return deployments;
|
|
193
|
+
}
|
|
194
|
+
function scaffoldManifests(coreDeployPath, meta) {
|
|
195
|
+
const templatesDir = node_path_1.default.join(coreDeployPath, 'k8s/templates');
|
|
196
|
+
const targetDir = node_path_1.default.join(coreDeployPath, 'k8s/apps', meta.namespace);
|
|
197
|
+
if (!node_fs_1.default.existsSync(targetDir)) {
|
|
198
|
+
node_fs_1.default.mkdirSync(targetDir, { recursive: true });
|
|
199
|
+
}
|
|
200
|
+
const templates = [
|
|
201
|
+
'namespace.yaml',
|
|
202
|
+
'pvc.yaml',
|
|
203
|
+
'configmap.yaml',
|
|
204
|
+
'secret.yaml',
|
|
205
|
+
'deployment.yaml',
|
|
206
|
+
'service.yaml',
|
|
207
|
+
'ingressroute.yaml'
|
|
208
|
+
];
|
|
209
|
+
const authUserB64 = Buffer.from(meta.authUser).toString('base64');
|
|
210
|
+
const authPassB64 = Buffer.from(meta.authPass).toString('base64');
|
|
211
|
+
for (const file of templates) {
|
|
212
|
+
const srcFile = node_path_1.default.join(templatesDir, file);
|
|
213
|
+
const destFile = node_path_1.default.join(targetDir, file);
|
|
214
|
+
if (!node_fs_1.default.existsSync(srcFile)) {
|
|
215
|
+
throw new Error(`Template missing: ${srcFile}`);
|
|
216
|
+
}
|
|
217
|
+
let content = node_fs_1.default.readFileSync(srcFile, 'utf8');
|
|
218
|
+
content = content.replace(/{{NAMESPACE}}/g, meta.namespace);
|
|
219
|
+
content = content.replace(/{{APP_NAME}}/g, meta.appName);
|
|
220
|
+
content = content.replace(/{{IMAGE_REF}}/g, meta.imageRef);
|
|
221
|
+
content = content.replace(/{{AUTH_USER_B64}}/g, authUserB64);
|
|
222
|
+
content = content.replace(/{{AUTH_PASS_B64}}/g, authPassB64);
|
|
223
|
+
content = content.replace(/{{DOMAIN}}/g, meta.domain);
|
|
224
|
+
content = content.replace(/{{CPU_REQUESTS}}/g, meta.resources.cpuRequests);
|
|
225
|
+
content = content.replace(/{{CPU_LIMITS}}/g, meta.resources.cpuLimits);
|
|
226
|
+
content = content.replace(/{{MEM_REQUESTS}}/g, meta.resources.memRequests);
|
|
227
|
+
content = content.replace(/{{MEM_LIMITS}}/g, meta.resources.memLimits);
|
|
228
|
+
content = content.replace(/{{DATA_PVC_SIZE}}/g, meta.resources.dataPvcSize);
|
|
229
|
+
content = content.replace(/{{STORAGE_PVC_SIZE}}/g, meta.resources.storagePvcSize);
|
|
230
|
+
node_fs_1.default.writeFileSync(destFile, content, 'utf8');
|
|
231
|
+
}
|
|
232
|
+
node_fs_1.default.writeFileSync(node_path_1.default.join(targetDir, 'metadata.json'), JSON.stringify(meta, null, 3), 'utf8');
|
|
233
|
+
}
|
|
234
|
+
function applyManifests(coreDeployPath, namespace) {
|
|
235
|
+
const targetDir = node_path_1.default.join(coreDeployPath, 'k8s/apps', namespace);
|
|
236
|
+
const manifestFiles = [
|
|
237
|
+
'namespace.yaml',
|
|
238
|
+
'pvc.yaml',
|
|
239
|
+
'configmap.yaml',
|
|
240
|
+
'secret.yaml',
|
|
241
|
+
'deployment.yaml',
|
|
242
|
+
'service.yaml',
|
|
243
|
+
'ingressroute.yaml'
|
|
244
|
+
];
|
|
245
|
+
const filesArgs = manifestFiles
|
|
246
|
+
.map(f => `-f "${node_path_1.default.join(targetDir, f)}"`)
|
|
247
|
+
.join(' ');
|
|
248
|
+
const cmd = `kubectl apply ${filesArgs}`;
|
|
249
|
+
console.log(`Executing: ${cmd}`);
|
|
250
|
+
return (0, node_child_process_1.execSync)(cmd, { encoding: 'utf8' });
|
|
251
|
+
}
|
|
252
|
+
async function deployCommand() {
|
|
253
|
+
console.log('\n==============================================');
|
|
254
|
+
console.log(' Quatrain Core Studio - Deployment Manager ');
|
|
255
|
+
console.log('==============================================\n');
|
|
256
|
+
let coreDeployPath = discoverCoreDeployPath();
|
|
257
|
+
if (coreDeployPath) {
|
|
258
|
+
console.log(`📡 Auto-detected CoreDeploy at: \x1b[33m${coreDeployPath}\x1b[0m`);
|
|
259
|
+
}
|
|
260
|
+
else {
|
|
261
|
+
console.log('⚠️ Could not auto-detect the CoreDeploy repository directory.');
|
|
262
|
+
const pathAnswer = await inquirer_1.default.prompt([
|
|
263
|
+
{
|
|
264
|
+
type: 'input',
|
|
265
|
+
name: 'path',
|
|
266
|
+
message: 'Please provide the absolute path to CoreDeploy:',
|
|
267
|
+
validate: (input) => {
|
|
268
|
+
if (!input || !node_fs_1.default.existsSync(node_path_1.default.join(input, 'k8s/templates/namespace.yaml'))) {
|
|
269
|
+
return 'Invalid directory. Ensure it is the root of CoreDeploy containing k8s/templates/namespace.yaml.';
|
|
270
|
+
}
|
|
271
|
+
return true;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
]);
|
|
275
|
+
coreDeployPath = node_path_1.default.resolve(pathAnswer.path);
|
|
276
|
+
}
|
|
277
|
+
let exitCli = false;
|
|
278
|
+
while (!exitCli) {
|
|
279
|
+
const { action } = await inquirer_1.default.prompt([
|
|
280
|
+
{
|
|
281
|
+
type: 'list',
|
|
282
|
+
name: 'action',
|
|
283
|
+
message: 'What deployment action would you like to perform?',
|
|
284
|
+
choices: [
|
|
285
|
+
{ name: '📋 List deployments', value: 'list' },
|
|
286
|
+
{ name: '✨ Create new deployment', value: 'create' },
|
|
287
|
+
{ name: '⚙️ Modify existing deployment', value: 'modify' },
|
|
288
|
+
{ name: '🚀 Promote deployment (Dev -> Prod)', value: 'promote' },
|
|
289
|
+
{ name: '❌ Delete deployment', value: 'delete' },
|
|
290
|
+
{ name: '🚪 Exit', value: 'exit' }
|
|
291
|
+
]
|
|
292
|
+
}
|
|
293
|
+
]);
|
|
294
|
+
try {
|
|
295
|
+
switch (action) {
|
|
296
|
+
case 'list': {
|
|
297
|
+
const deployments = loadAllDeployments(coreDeployPath);
|
|
298
|
+
if (deployments.length === 0) {
|
|
299
|
+
console.log('\nℹ️ No deployments found.\n');
|
|
300
|
+
break;
|
|
301
|
+
}
|
|
302
|
+
console.log('\nActive Studio Deployments:');
|
|
303
|
+
console.log('------------------------------------------------------------------------------------------------------------------------');
|
|
304
|
+
console.log(`${'App Name'.padEnd(15)} | ${'Namespace'.padEnd(23)} | ${'Env'.padEnd(4)} | ${'FQDN Access Domain'.padEnd(35)} | ${'Image Reference'.padEnd(35)}`);
|
|
305
|
+
console.log('------------------------------------------------------------------------------------------------------------------------');
|
|
306
|
+
deployments.forEach(d => {
|
|
307
|
+
console.log(`${d.appName.padEnd(15)} | ${d.namespace.padEnd(23)} | ${d.env.padEnd(4)} | ${d.domain.padEnd(35)} | ${d.imageRef.padEnd(35)}`);
|
|
308
|
+
});
|
|
309
|
+
console.log('------------------------------------------------------------------------------------------------------------------------\n');
|
|
310
|
+
break;
|
|
311
|
+
}
|
|
312
|
+
case 'create': {
|
|
313
|
+
const answers = await inquirer_1.default.prompt([
|
|
314
|
+
{
|
|
315
|
+
type: 'input',
|
|
316
|
+
name: 'appName',
|
|
317
|
+
message: 'Nom de l\'application (alphanumeric/dashes only):',
|
|
318
|
+
validate: (input) => {
|
|
319
|
+
if (!input || !/^[a-zA-Z0-9\-]+$/.test(input)) {
|
|
320
|
+
return 'Application name is required and can only contain letters, numbers, and dashes.';
|
|
321
|
+
}
|
|
322
|
+
return true;
|
|
323
|
+
}
|
|
324
|
+
},
|
|
325
|
+
{
|
|
326
|
+
type: 'list',
|
|
327
|
+
name: 'env',
|
|
328
|
+
message: 'Environnement (dev / prod) :',
|
|
329
|
+
choices: [
|
|
330
|
+
{ name: 'Development (suffixed with -dev)', value: 'dev' },
|
|
331
|
+
{ name: 'Production', value: 'prod' }
|
|
332
|
+
]
|
|
333
|
+
}
|
|
334
|
+
]);
|
|
335
|
+
const appNameClean = answers.appName.toLowerCase();
|
|
336
|
+
const suffix = Math.random().toString(36).substring(2, 7);
|
|
337
|
+
let defaultNamespace = `${appNameClean}-${suffix}`;
|
|
338
|
+
let defaultDomain = `${appNameClean}-${suffix}.quatrain.app`;
|
|
339
|
+
if (answers.env === 'dev') {
|
|
340
|
+
defaultNamespace += '-dev';
|
|
341
|
+
defaultDomain = `${appNameClean}-${suffix}.quatrain.dev`;
|
|
342
|
+
}
|
|
343
|
+
const flowAnswers = await inquirer_1.default.prompt([
|
|
344
|
+
{
|
|
345
|
+
type: 'input',
|
|
346
|
+
name: 'domain',
|
|
347
|
+
message: `Subdomain Access FQDN (Par défaut: ${defaultDomain}) :`,
|
|
348
|
+
default: defaultDomain
|
|
349
|
+
},
|
|
350
|
+
{
|
|
351
|
+
type: 'input',
|
|
352
|
+
name: 'imageRef',
|
|
353
|
+
message: `Référence de l'image (Par défaut: studio-image:${detectLatestStableTag(coreDeployPath)}) :`,
|
|
354
|
+
default: `ghcr.io/quatrain/studio-image:${detectLatestStableTag(coreDeployPath)}`
|
|
355
|
+
},
|
|
356
|
+
{
|
|
357
|
+
type: 'input',
|
|
358
|
+
name: 'authUser',
|
|
359
|
+
message: 'Auth Username (Par défaut: admin) :',
|
|
360
|
+
default: 'admin'
|
|
361
|
+
},
|
|
362
|
+
{
|
|
363
|
+
type: 'input',
|
|
364
|
+
name: 'authPass',
|
|
365
|
+
message: `Auth Password (Par défaut (généré sécurisé) :`,
|
|
366
|
+
default: () => generateSecurePassword()
|
|
367
|
+
},
|
|
368
|
+
{
|
|
369
|
+
type: 'list',
|
|
370
|
+
name: 'resourceChoice',
|
|
371
|
+
message: 'Configure Resources / Physical Storage:',
|
|
372
|
+
choices: [
|
|
373
|
+
{ name: 'Use Default Limits (CPU req: 100m, limit: 500m | Mem req: 256Mi, limit: 512Mi | Data: 1Gi, Storage: 10Gi)', value: 'default' },
|
|
374
|
+
{ name: 'Customize Resources & Storage', value: 'custom' }
|
|
375
|
+
]
|
|
376
|
+
}
|
|
377
|
+
]);
|
|
378
|
+
let resources = {
|
|
379
|
+
cpuRequests: '100m',
|
|
380
|
+
cpuLimits: '500m',
|
|
381
|
+
memRequests: '256Mi',
|
|
382
|
+
memLimits: '512Mi',
|
|
383
|
+
dataPvcSize: '1Gi',
|
|
384
|
+
storagePvcSize: '10Gi'
|
|
385
|
+
};
|
|
386
|
+
if (flowAnswers.resourceChoice === 'custom') {
|
|
387
|
+
const customRes = await inquirer_1.default.prompt([
|
|
388
|
+
{
|
|
389
|
+
type: 'input',
|
|
390
|
+
name: 'cpuRequests',
|
|
391
|
+
message: 'CPU Requests (e.g. 100m, 50m) :',
|
|
392
|
+
default: '100m'
|
|
393
|
+
},
|
|
394
|
+
{
|
|
395
|
+
type: 'input',
|
|
396
|
+
name: 'cpuLimits',
|
|
397
|
+
message: 'CPU Limits (e.g. 500m, 200m) :',
|
|
398
|
+
default: '500m'
|
|
399
|
+
},
|
|
400
|
+
{
|
|
401
|
+
type: 'input',
|
|
402
|
+
name: 'memRequests',
|
|
403
|
+
message: 'Memory Requests (e.g. 256Mi, 128Mi) :',
|
|
404
|
+
default: '256Mi'
|
|
405
|
+
},
|
|
406
|
+
{
|
|
407
|
+
type: 'input',
|
|
408
|
+
name: 'memLimits',
|
|
409
|
+
message: 'Memory Limits (e.g. 512Mi, 256Mi) :',
|
|
410
|
+
default: '512Mi'
|
|
411
|
+
},
|
|
412
|
+
{
|
|
413
|
+
type: 'input',
|
|
414
|
+
name: 'dataPvcSize',
|
|
415
|
+
message: 'Data PVC Storage Capacity (e.g. 1Gi, 2Gi) :',
|
|
416
|
+
default: '1Gi'
|
|
417
|
+
},
|
|
418
|
+
{
|
|
419
|
+
type: 'input',
|
|
420
|
+
name: 'storagePvcSize',
|
|
421
|
+
message: 'Storage PVC Storage Capacity (e.g. 10Gi, 5Gi) :',
|
|
422
|
+
default: '10Gi'
|
|
423
|
+
}
|
|
424
|
+
]);
|
|
425
|
+
resources = customRes;
|
|
426
|
+
}
|
|
427
|
+
const meta = {
|
|
428
|
+
appName: appNameClean,
|
|
429
|
+
namespace: defaultNamespace,
|
|
430
|
+
env: answers.env,
|
|
431
|
+
domain: flowAnswers.domain,
|
|
432
|
+
imageRef: flowAnswers.imageRef,
|
|
433
|
+
authUser: flowAnswers.authUser,
|
|
434
|
+
authPass: flowAnswers.authPass,
|
|
435
|
+
resources,
|
|
436
|
+
createdAt: new Date().toISOString()
|
|
437
|
+
};
|
|
438
|
+
scaffoldManifests(coreDeployPath, meta);
|
|
439
|
+
console.log(`\n✅ Manifests and metadata successfully scaffolded under: k8s/apps/${meta.namespace}/`);
|
|
440
|
+
console.log(`🌐 FQDN access URL: https://${meta.domain}`);
|
|
441
|
+
console.log(`🔑 Credentials : ${meta.authUser} / ${meta.authPass}\n`);
|
|
442
|
+
const { applyNow } = await inquirer_1.default.prompt([
|
|
443
|
+
{
|
|
444
|
+
type: 'confirm',
|
|
445
|
+
name: 'applyNow',
|
|
446
|
+
message: `Do you want to deploy ${meta.namespace} immediately to the Kubernetes cluster?`,
|
|
447
|
+
default: true
|
|
448
|
+
}
|
|
449
|
+
]);
|
|
450
|
+
if (applyNow) {
|
|
451
|
+
const output = applyManifests(coreDeployPath, meta.namespace);
|
|
452
|
+
console.log(`\x1b[32m${output}\x1b[0m`);
|
|
453
|
+
}
|
|
454
|
+
break;
|
|
455
|
+
}
|
|
456
|
+
case 'modify': {
|
|
457
|
+
const deployments = loadAllDeployments(coreDeployPath);
|
|
458
|
+
if (deployments.length === 0) {
|
|
459
|
+
console.log('\nℹ️ No deployments found to modify.\n');
|
|
460
|
+
break;
|
|
461
|
+
}
|
|
462
|
+
const { targetNs } = await inquirer_1.default.prompt([
|
|
463
|
+
{
|
|
464
|
+
type: 'list',
|
|
465
|
+
name: 'targetNs',
|
|
466
|
+
message: 'Select the deployment to modify:',
|
|
467
|
+
choices: deployments.map(d => ({
|
|
468
|
+
name: `${d.namespace} (domain: ${d.domain})`,
|
|
469
|
+
value: d.namespace
|
|
470
|
+
}))
|
|
471
|
+
}
|
|
472
|
+
]);
|
|
473
|
+
const targetMeta = deployments.find(d => d.namespace === targetNs);
|
|
474
|
+
const updates = await inquirer_1.default.prompt([
|
|
475
|
+
{
|
|
476
|
+
type: 'input',
|
|
477
|
+
name: 'domain',
|
|
478
|
+
message: 'Subdomain Access FQDN:',
|
|
479
|
+
default: targetMeta.domain
|
|
480
|
+
},
|
|
481
|
+
{
|
|
482
|
+
type: 'input',
|
|
483
|
+
name: 'imageRef',
|
|
484
|
+
message: 'Image Reference:',
|
|
485
|
+
default: targetMeta.imageRef
|
|
486
|
+
},
|
|
487
|
+
{
|
|
488
|
+
type: 'input',
|
|
489
|
+
name: 'authUser',
|
|
490
|
+
message: 'Auth Username:',
|
|
491
|
+
default: targetMeta.authUser
|
|
492
|
+
},
|
|
493
|
+
{
|
|
494
|
+
type: 'input',
|
|
495
|
+
name: 'authPass',
|
|
496
|
+
message: 'Auth Password:',
|
|
497
|
+
default: targetMeta.authPass
|
|
498
|
+
},
|
|
499
|
+
{
|
|
500
|
+
type: 'input',
|
|
501
|
+
name: 'cpuRequests',
|
|
502
|
+
message: 'CPU Requests:',
|
|
503
|
+
default: targetMeta.resources?.cpuRequests || '100m'
|
|
504
|
+
},
|
|
505
|
+
{
|
|
506
|
+
type: 'input',
|
|
507
|
+
name: 'cpuLimits',
|
|
508
|
+
message: 'CPU Limits:',
|
|
509
|
+
default: targetMeta.resources?.cpuLimits || '500m'
|
|
510
|
+
},
|
|
511
|
+
{
|
|
512
|
+
type: 'input',
|
|
513
|
+
name: 'memRequests',
|
|
514
|
+
message: 'Memory Requests:',
|
|
515
|
+
default: targetMeta.resources?.memRequests || '256Mi'
|
|
516
|
+
},
|
|
517
|
+
{
|
|
518
|
+
type: 'input',
|
|
519
|
+
name: 'memLimits',
|
|
520
|
+
message: 'Memory Limits:',
|
|
521
|
+
default: targetMeta.resources?.memLimits || '512Mi'
|
|
522
|
+
},
|
|
523
|
+
{
|
|
524
|
+
type: 'input',
|
|
525
|
+
name: 'dataPvcSize',
|
|
526
|
+
message: 'Data PVC storage size:',
|
|
527
|
+
default: targetMeta.resources?.dataPvcSize || '1Gi'
|
|
528
|
+
},
|
|
529
|
+
{
|
|
530
|
+
type: 'input',
|
|
531
|
+
name: 'storagePvcSize',
|
|
532
|
+
message: 'Storage PVC storage size:',
|
|
533
|
+
default: targetMeta.resources?.storagePvcSize || '10Gi'
|
|
534
|
+
}
|
|
535
|
+
]);
|
|
536
|
+
const updatedMeta = {
|
|
537
|
+
...targetMeta,
|
|
538
|
+
domain: updates.domain,
|
|
539
|
+
imageRef: updates.imageRef,
|
|
540
|
+
authUser: updates.authUser,
|
|
541
|
+
authPass: updates.authPass,
|
|
542
|
+
resources: {
|
|
543
|
+
cpuRequests: updates.cpuRequests,
|
|
544
|
+
cpuLimits: updates.cpuLimits,
|
|
545
|
+
memRequests: updates.memRequests,
|
|
546
|
+
memLimits: updates.memLimits,
|
|
547
|
+
dataPvcSize: updates.dataPvcSize,
|
|
548
|
+
storagePvcSize: updates.storagePvcSize
|
|
549
|
+
},
|
|
550
|
+
updatedAt: new Date().toISOString()
|
|
551
|
+
};
|
|
552
|
+
scaffoldManifests(coreDeployPath, updatedMeta);
|
|
553
|
+
console.log(`\n✅ Manifests and metadata updated for namespace ${targetNs}.`);
|
|
554
|
+
const { applyNow } = await inquirer_1.default.prompt([
|
|
555
|
+
{
|
|
556
|
+
type: 'confirm',
|
|
557
|
+
name: 'applyNow',
|
|
558
|
+
message: 'Do you want to apply these updates to the cluster now?',
|
|
559
|
+
default: true
|
|
560
|
+
}
|
|
561
|
+
]);
|
|
562
|
+
if (applyNow) {
|
|
563
|
+
const output = applyManifests(coreDeployPath, targetNs);
|
|
564
|
+
console.log(`\x1b[32m${output}\x1b[0m`);
|
|
565
|
+
}
|
|
566
|
+
break;
|
|
567
|
+
}
|
|
568
|
+
case 'promote': {
|
|
569
|
+
const deployments = loadAllDeployments(coreDeployPath);
|
|
570
|
+
const devDeployments = deployments.filter(d => d.env === 'dev' || d.namespace.endsWith('-dev'));
|
|
571
|
+
if (devDeployments.length === 0) {
|
|
572
|
+
console.log('\nℹ️ No development deployments found to promote.\n');
|
|
573
|
+
break;
|
|
574
|
+
}
|
|
575
|
+
const { targetNs } = await inquirer_1.default.prompt([
|
|
576
|
+
{
|
|
577
|
+
type: 'list',
|
|
578
|
+
name: 'targetNs',
|
|
579
|
+
message: 'Select the development app to promote to production:',
|
|
580
|
+
choices: devDeployments.map(d => ({
|
|
581
|
+
name: `${d.namespace} (domain: ${d.domain})`,
|
|
582
|
+
value: d.namespace
|
|
583
|
+
}))
|
|
584
|
+
}
|
|
585
|
+
]);
|
|
586
|
+
const devMeta = devDeployments.find(d => d.namespace === targetNs);
|
|
587
|
+
const prodNamespace = devMeta.namespace.replace(/-dev$/, '');
|
|
588
|
+
const prodDomain = devMeta.domain.replace(/\.dev$/, '.app');
|
|
589
|
+
console.log(`\nPromoting installation to production:`);
|
|
590
|
+
console.log(` Dev Namespace : ${devMeta.namespace} -> Prod Namespace : \x1b[32m${prodNamespace}\x1b[0m`);
|
|
591
|
+
console.log(` Dev Domain : ${devMeta.domain} -> Prod Domain : \x1b[32m${prodDomain}\x1b[0m\n`);
|
|
592
|
+
const confirmPromote = await inquirer_1.default.prompt([
|
|
593
|
+
{
|
|
594
|
+
type: 'confirm',
|
|
595
|
+
name: 'confirm',
|
|
596
|
+
message: 'Do you want to proceed with this promotion?',
|
|
597
|
+
default: true
|
|
598
|
+
}
|
|
599
|
+
]);
|
|
600
|
+
if (!confirmPromote.confirm) {
|
|
601
|
+
console.log('Promotion cancelled.\n');
|
|
602
|
+
break;
|
|
603
|
+
}
|
|
604
|
+
const prodMeta = {
|
|
605
|
+
appName: devMeta.appName,
|
|
606
|
+
namespace: prodNamespace,
|
|
607
|
+
env: 'prod',
|
|
608
|
+
domain: prodDomain,
|
|
609
|
+
imageRef: devMeta.imageRef,
|
|
610
|
+
authUser: devMeta.authUser,
|
|
611
|
+
authPass: devMeta.authPass,
|
|
612
|
+
resources: { ...devMeta.resources },
|
|
613
|
+
createdAt: new Date().toISOString()
|
|
614
|
+
};
|
|
615
|
+
scaffoldManifests(coreDeployPath, prodMeta);
|
|
616
|
+
console.log(`\n✅ Production manifests scaffolded under: k8s/apps/${prodNamespace}/`);
|
|
617
|
+
const { applyNow } = await inquirer_1.default.prompt([
|
|
618
|
+
{
|
|
619
|
+
type: 'confirm',
|
|
620
|
+
name: 'applyNow',
|
|
621
|
+
message: `Do you want to deploy ${prodNamespace} immediately to production?`,
|
|
622
|
+
default: true
|
|
623
|
+
}
|
|
624
|
+
]);
|
|
625
|
+
if (applyNow) {
|
|
626
|
+
const output = applyManifests(coreDeployPath, prodNamespace);
|
|
627
|
+
console.log(`\x1b[32m${output}\x1b[0m`);
|
|
628
|
+
}
|
|
629
|
+
break;
|
|
630
|
+
}
|
|
631
|
+
case 'delete': {
|
|
632
|
+
const deployments = loadAllDeployments(coreDeployPath);
|
|
633
|
+
if (deployments.length === 0) {
|
|
634
|
+
console.log('\nℹ️ No deployments found to delete.\n');
|
|
635
|
+
break;
|
|
636
|
+
}
|
|
637
|
+
const { targetNs } = await inquirer_1.default.prompt([
|
|
638
|
+
{
|
|
639
|
+
type: 'list',
|
|
640
|
+
name: 'targetNs',
|
|
641
|
+
message: 'Select the deployment to delete:',
|
|
642
|
+
choices: deployments.map(d => ({
|
|
643
|
+
name: `${d.namespace} (domain: ${d.domain})`,
|
|
644
|
+
value: d.namespace
|
|
645
|
+
}))
|
|
646
|
+
}
|
|
647
|
+
]);
|
|
648
|
+
const { confirmDelete } = await inquirer_1.default.prompt([
|
|
649
|
+
{
|
|
650
|
+
type: 'confirm',
|
|
651
|
+
name: 'confirmDelete',
|
|
652
|
+
message: `Are you sure you want to delete local manifests & config for ${targetNs}? (This action is irreversible!)`,
|
|
653
|
+
default: false
|
|
654
|
+
}
|
|
655
|
+
]);
|
|
656
|
+
if (!confirmDelete) {
|
|
657
|
+
console.log('Deletion cancelled.\n');
|
|
658
|
+
break;
|
|
659
|
+
}
|
|
660
|
+
const { deleteK8s } = await inquirer_1.default.prompt([
|
|
661
|
+
{
|
|
662
|
+
type: 'confirm',
|
|
663
|
+
name: 'deleteK8s',
|
|
664
|
+
message: `Do you also want to delete the namespace "${targetNs}" from the active Kubernetes cluster?`,
|
|
665
|
+
default: true
|
|
666
|
+
}
|
|
667
|
+
]);
|
|
668
|
+
if (deleteK8s) {
|
|
669
|
+
console.log(`Executing: kubectl delete namespace ${targetNs}`);
|
|
670
|
+
try {
|
|
671
|
+
const output = (0, node_child_process_1.execSync)(`kubectl delete namespace "${targetNs}"`, { encoding: 'utf8' });
|
|
672
|
+
console.log(`\x1b[32m${output}\x1b[0m`);
|
|
673
|
+
}
|
|
674
|
+
catch (err) {
|
|
675
|
+
console.error(`Failed to delete namespace ${targetNs} on the cluster:`, err.message);
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
const appDir = node_path_1.default.join(coreDeployPath, 'k8s/apps', targetNs);
|
|
679
|
+
if (node_fs_1.default.existsSync(appDir)) {
|
|
680
|
+
node_fs_1.default.rmSync(appDir, { recursive: true, force: true });
|
|
681
|
+
console.log(`✅ Deleted local directory: k8s/apps/${targetNs}\n`);
|
|
682
|
+
}
|
|
683
|
+
break;
|
|
684
|
+
}
|
|
685
|
+
case 'exit':
|
|
686
|
+
exitCli = true;
|
|
687
|
+
break;
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
catch (err) {
|
|
691
|
+
console.error(`\n❌ Error performing action: ${err.message}\n`);
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
console.log('🚪 Exiting Deployment Manager.');
|
|
695
|
+
}
|
|
696
|
+
//# sourceMappingURL=deploy.js.map
|