@hed-hog/cli 0.0.36 ā 0.0.37
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/dist/package.json +1 -1
- package/dist/src/app.module.js +2 -0
- package/dist/src/app.module.js.map +1 -1
- package/dist/src/commands/dev.command/deploy-config.subcommand.d.ts +10 -0
- package/dist/src/commands/dev.command/deploy-config.subcommand.js +47 -0
- package/dist/src/commands/dev.command/deploy-config.subcommand.js.map +1 -0
- package/dist/src/commands/dev.command.js +2 -0
- package/dist/src/commands/dev.command.js.map +1 -1
- package/dist/src/modules/developer/developer.service.d.ts +29 -0
- package/dist/src/modules/developer/developer.service.js +1368 -0
- package/dist/src/modules/developer/developer.service.js.map +1 -1
- package/dist/src/modules/runner/runner.service.d.ts +7 -1
- package/dist/src/modules/runner/runner.service.js +6 -0
- package/dist/src/modules/runner/runner.service.js.map +1 -1
- package/dist/src/templates/deployment/.dockerignore.ejs +9 -0
- package/dist/src/templates/deployment/admin.Dockerfile.ejs +14 -0
- package/dist/src/templates/deployment/api.Dockerfile.ejs +12 -0
- package/dist/src/templates/library/init.package.json.ejs +4 -1
- package/dist/src/templates/library/package.json.ejs +3 -2
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +1 -1
|
@@ -15,6 +15,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
15
15
|
exports.DeveloperService = void 0;
|
|
16
16
|
const common_1 = require("@nestjs/common");
|
|
17
17
|
const chalk = require("chalk");
|
|
18
|
+
const child_process_1 = require("child_process");
|
|
18
19
|
const crypto_1 = require("crypto");
|
|
19
20
|
const ejs_1 = require("ejs");
|
|
20
21
|
const fs_1 = require("fs");
|
|
@@ -51,6 +52,1373 @@ let DeveloperService = class DeveloperService {
|
|
|
51
52
|
process.emitWarning = originalEmitWarning;
|
|
52
53
|
};
|
|
53
54
|
}
|
|
55
|
+
refreshEnvironmentPath() {
|
|
56
|
+
try {
|
|
57
|
+
if (process.platform === 'win32') {
|
|
58
|
+
// On Windows, re-read PATH from system environment registry
|
|
59
|
+
const pathCmd = `powershell -NoProfile -Command "[System.Environment]::GetEnvironmentVariable('Path', 'Machine') + ';' + [System.Environment]::GetEnvironmentVariable('Path', 'User')"`;
|
|
60
|
+
const newPath = (0, child_process_1.execSync)(pathCmd, { encoding: 'utf8' }).trim();
|
|
61
|
+
if (newPath) {
|
|
62
|
+
process.env.PATH = newPath;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
else if (process.platform === 'darwin') {
|
|
66
|
+
// On macOS, try to refresh PATH
|
|
67
|
+
try {
|
|
68
|
+
const newPath = (0, child_process_1.execSync)('eval "$(launchctl getenv PATH)" && echo $PATH', {
|
|
69
|
+
encoding: 'utf8',
|
|
70
|
+
shell: '/bin/bash',
|
|
71
|
+
}).trim();
|
|
72
|
+
if (newPath) {
|
|
73
|
+
process.env.PATH = newPath;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
// Silent fail - macOS might not have launchctl env
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
// On Linux, source bashrc and get PATH
|
|
82
|
+
try {
|
|
83
|
+
const newPath = (0, child_process_1.execSync)('bash -ic "echo $PATH"', {
|
|
84
|
+
encoding: 'utf8',
|
|
85
|
+
}).trim();
|
|
86
|
+
if (newPath) {
|
|
87
|
+
process.env.PATH = newPath;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
// Silent fail
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
// Silent fail - if refresh doesn't work, tools check will handle it
|
|
97
|
+
this.log('Environment refresh attempt completed');
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
async deployConfig(path, verbose = false) {
|
|
101
|
+
const restoreWarnings = this.suppressWarnings();
|
|
102
|
+
this.verbose = verbose;
|
|
103
|
+
path = await this.getRootPath(path);
|
|
104
|
+
const spinner = ora('Checking deployment configuration...').start();
|
|
105
|
+
try {
|
|
106
|
+
spinner.text = 'Checking required tools...';
|
|
107
|
+
// Check all required tools
|
|
108
|
+
const toolsStatus = await this.checkRequiredTools();
|
|
109
|
+
spinner.stop();
|
|
110
|
+
// Display tools status
|
|
111
|
+
console.log(chalk.blue.bold('\nš§ Required Tools Status:\n'));
|
|
112
|
+
const missingTools = [];
|
|
113
|
+
for (const [tool, status] of Object.entries(toolsStatus)) {
|
|
114
|
+
if (status.installed) {
|
|
115
|
+
console.log(chalk.green(` ā ${tool.padEnd(15)} ${status.version || 'installed'}`));
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
console.log(chalk.red(` ā ${tool.padEnd(15)} not installed`));
|
|
119
|
+
missingTools.push(tool);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
// Handle missing tools
|
|
123
|
+
if (missingTools.length > 0) {
|
|
124
|
+
console.log(chalk.yellow('\nā ļø Some required tools are missing.\n'));
|
|
125
|
+
const { installMissing } = await inquirer_1.default.prompt([
|
|
126
|
+
{
|
|
127
|
+
type: 'confirm',
|
|
128
|
+
name: 'installMissing',
|
|
129
|
+
message: 'Would you like help installing and configuring the missing tools?',
|
|
130
|
+
default: true,
|
|
131
|
+
},
|
|
132
|
+
]);
|
|
133
|
+
if (installMissing) {
|
|
134
|
+
await this.helpInstallTools(missingTools);
|
|
135
|
+
// Refresh environment variables to pick up newly installed tools
|
|
136
|
+
this.refreshEnvironmentPath();
|
|
137
|
+
// Re-check tools after installation
|
|
138
|
+
console.log(chalk.blue('\nš Re-checking tools...\n'));
|
|
139
|
+
const newToolsStatus = await this.checkRequiredTools();
|
|
140
|
+
const stillMissing = Object.entries(newToolsStatus)
|
|
141
|
+
.filter(([_, status]) => !status.installed)
|
|
142
|
+
.map(([tool]) => tool);
|
|
143
|
+
if (stillMissing.length > 0) {
|
|
144
|
+
console.log(chalk.red('\nā The following tools are still not available:'));
|
|
145
|
+
stillMissing.forEach((tool) => console.log(chalk.red(` ⢠${tool}`)));
|
|
146
|
+
console.log(chalk.yellow('\nPlease install them manually and run this command again.\n'));
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
console.log(chalk.yellow('\nā ļø Cannot proceed without required tools.'));
|
|
152
|
+
console.log(chalk.blue('\nPlease install the following tools:\n'));
|
|
153
|
+
missingTools.forEach((tool) => {
|
|
154
|
+
console.log(chalk.white(` ${tool}:`));
|
|
155
|
+
this.printInstallInstructions(tool);
|
|
156
|
+
console.log('');
|
|
157
|
+
});
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
console.log(chalk.green('\nā All required tools are available!\n'));
|
|
162
|
+
// Start deployment configuration wizard
|
|
163
|
+
const config = await this.runDeploymentWizard();
|
|
164
|
+
if (!config) {
|
|
165
|
+
console.log(chalk.yellow('\nDeployment configuration cancelled.\n'));
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
// Generate deployment files
|
|
169
|
+
spinner.start('Generating deployment configuration files...');
|
|
170
|
+
await this.generateDeploymentFiles(path, config);
|
|
171
|
+
spinner.succeed(chalk.green('Deployment configuration completed successfully!'));
|
|
172
|
+
// Display summary
|
|
173
|
+
this.displayDeploymentSummary(config);
|
|
174
|
+
}
|
|
175
|
+
catch (error) {
|
|
176
|
+
spinner.fail('Failed to configure deployment.');
|
|
177
|
+
console.error(chalk.red('Error configuring deployment:'), error);
|
|
178
|
+
throw error;
|
|
179
|
+
}
|
|
180
|
+
finally {
|
|
181
|
+
restoreWarnings();
|
|
182
|
+
spinner.stop();
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
async checkRequiredTools() {
|
|
186
|
+
const tools = {
|
|
187
|
+
kubectl: this.checkKubectl.bind(this),
|
|
188
|
+
doctl: this.checkDoctl.bind(this),
|
|
189
|
+
'gh cli': this.checkGhCli.bind(this),
|
|
190
|
+
helm: this.checkHelm.bind(this),
|
|
191
|
+
};
|
|
192
|
+
const results = {};
|
|
193
|
+
for (const [name, checkFn] of Object.entries(tools)) {
|
|
194
|
+
try {
|
|
195
|
+
const version = await checkFn();
|
|
196
|
+
results[name] = { installed: true, version };
|
|
197
|
+
}
|
|
198
|
+
catch {
|
|
199
|
+
results[name] = { installed: false };
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return results;
|
|
203
|
+
}
|
|
204
|
+
async checkKubectl() {
|
|
205
|
+
const result = await this.runner.executeCommand(runner_service_1.ProgramName.KUBECTL, ['version', '--client'], {}, true);
|
|
206
|
+
// Extract version from output (works for both old and new kubectl versions)
|
|
207
|
+
const match = result.stdout.match(/Client Version: (v[\d.]+)|GitVersion:"(v[\d.]+)"/);
|
|
208
|
+
return match ? match[1] || match[2] : result.stdout.split('\n')[0].trim();
|
|
209
|
+
}
|
|
210
|
+
async checkDoctl() {
|
|
211
|
+
const result = await this.runner.executeCommand(runner_service_1.ProgramName.DOCTL, ['version'], {}, true);
|
|
212
|
+
return result.stdout.trim();
|
|
213
|
+
}
|
|
214
|
+
async checkGhCli() {
|
|
215
|
+
const result = await this.runner.executeCommand(runner_service_1.ProgramName.GH, ['--version'], {}, true);
|
|
216
|
+
const versionLine = result.stdout.split('\n')[0];
|
|
217
|
+
return versionLine.trim();
|
|
218
|
+
}
|
|
219
|
+
async checkHelm() {
|
|
220
|
+
const result = await this.runner.executeCommand(runner_service_1.ProgramName.HELM, ['version', '--short'], {}, true);
|
|
221
|
+
// Extract version (handles both --short and regular output)
|
|
222
|
+
const versionMatch = result.stdout.match(/v[\d.]+/);
|
|
223
|
+
return versionMatch ? versionMatch[0] : result.stdout.trim();
|
|
224
|
+
}
|
|
225
|
+
async checkPackageManager(manager) {
|
|
226
|
+
try {
|
|
227
|
+
if (manager === 'choco') {
|
|
228
|
+
await this.runner.executeCommand(runner_service_1.ProgramName.POWERSHELL, ['-Command', 'choco --version'], {}, true);
|
|
229
|
+
return true;
|
|
230
|
+
}
|
|
231
|
+
else if (manager === 'winget') {
|
|
232
|
+
await this.runner.executeCommand(runner_service_1.ProgramName.POWERSHELL, ['-Command', 'winget --version'], {}, true);
|
|
233
|
+
return true;
|
|
234
|
+
}
|
|
235
|
+
else if (manager === 'scoop') {
|
|
236
|
+
await this.runner.executeCommand(runner_service_1.ProgramName.POWERSHELL, ['-Command', 'scoop --version'], {}, true);
|
|
237
|
+
return true;
|
|
238
|
+
}
|
|
239
|
+
else if (manager === 'brew') {
|
|
240
|
+
await this.runner.executeCommand(runner_service_1.ProgramName.BREW, ['--version'], {}, true);
|
|
241
|
+
return true;
|
|
242
|
+
}
|
|
243
|
+
else if (manager === 'apt') {
|
|
244
|
+
await this.runner.executeCommand(runner_service_1.ProgramName.POWERSHELL, ['-Command', 'apt --version'], {}, true);
|
|
245
|
+
return true;
|
|
246
|
+
}
|
|
247
|
+
else if (manager === 'snap') {
|
|
248
|
+
await this.runner.executeCommand(runner_service_1.ProgramName.POWERSHELL, ['-Command', 'snap --version'], {}, true);
|
|
249
|
+
return true;
|
|
250
|
+
}
|
|
251
|
+
return false;
|
|
252
|
+
}
|
|
253
|
+
catch {
|
|
254
|
+
return false;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
async getAvailablePackageManager() {
|
|
258
|
+
const platform = process.platform;
|
|
259
|
+
if (platform === 'win32') {
|
|
260
|
+
// Check for Chocolatey first (more reliable for dev tools)
|
|
261
|
+
if (await this.checkPackageManager('choco')) {
|
|
262
|
+
return 'choco';
|
|
263
|
+
}
|
|
264
|
+
// Then check for Scoop (developer-friendly, no admin required)
|
|
265
|
+
if (await this.checkPackageManager('scoop')) {
|
|
266
|
+
return 'scoop';
|
|
267
|
+
}
|
|
268
|
+
// Finally check for winget
|
|
269
|
+
if (await this.checkPackageManager('winget')) {
|
|
270
|
+
return 'winget';
|
|
271
|
+
}
|
|
272
|
+
return null;
|
|
273
|
+
}
|
|
274
|
+
else if (platform === 'darwin') {
|
|
275
|
+
if (await this.checkPackageManager('brew')) {
|
|
276
|
+
return 'brew';
|
|
277
|
+
}
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
280
|
+
else if (platform === 'linux') {
|
|
281
|
+
// Check for apt first (most common on Ubuntu/Debian)
|
|
282
|
+
if (await this.checkPackageManager('apt')) {
|
|
283
|
+
return 'apt';
|
|
284
|
+
}
|
|
285
|
+
// Then check for snap (universal package manager)
|
|
286
|
+
if (await this.checkPackageManager('snap')) {
|
|
287
|
+
return 'snap';
|
|
288
|
+
}
|
|
289
|
+
return null;
|
|
290
|
+
}
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
293
|
+
async installChocolatey() {
|
|
294
|
+
console.log(chalk.blue('\nš¦ Installing Chocolatey...\n'));
|
|
295
|
+
try {
|
|
296
|
+
// Install Chocolatey using the official installation script
|
|
297
|
+
await this.runner.executeCommand(runner_service_1.ProgramName.POWERSHELL, [
|
|
298
|
+
'-NoProfile',
|
|
299
|
+
'-ExecutionPolicy',
|
|
300
|
+
'Bypass',
|
|
301
|
+
'-Command',
|
|
302
|
+
"Set-ExecutionPolicy Bypass -Scope Process -Force; [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1'))",
|
|
303
|
+
], {}, false);
|
|
304
|
+
console.log(chalk.green('\nā Chocolatey installed successfully!\n'));
|
|
305
|
+
return true;
|
|
306
|
+
}
|
|
307
|
+
catch (error) {
|
|
308
|
+
console.log(chalk.red('\nā Failed to install Chocolatey.\n'));
|
|
309
|
+
console.log(chalk.yellow('Please install it manually from: https://chocolatey.org/install\n'));
|
|
310
|
+
return false;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
async helpInstallTools(missingTools) {
|
|
314
|
+
console.log(chalk.blue('\nš¦ Installing and configuring tools...\n'));
|
|
315
|
+
// Check for available package manager
|
|
316
|
+
let packageManager = await this.getAvailablePackageManager();
|
|
317
|
+
// If on Windows and no package manager found, offer to install Chocolatey
|
|
318
|
+
if (process.platform === 'win32' && !packageManager) {
|
|
319
|
+
console.log(chalk.yellow('ā ļø No package manager detected (Chocolatey or winget).\n'));
|
|
320
|
+
const { installChoco } = await inquirer_1.default.prompt([
|
|
321
|
+
{
|
|
322
|
+
type: 'confirm',
|
|
323
|
+
name: 'installChoco',
|
|
324
|
+
message: 'Would you like to install Chocolatey to manage package installations?',
|
|
325
|
+
default: true,
|
|
326
|
+
},
|
|
327
|
+
]);
|
|
328
|
+
if (installChoco) {
|
|
329
|
+
const success = await this.installChocolatey();
|
|
330
|
+
if (success) {
|
|
331
|
+
packageManager = 'choco';
|
|
332
|
+
}
|
|
333
|
+
else {
|
|
334
|
+
console.log(chalk.yellow('\nCannot proceed with automatic installation without a package manager.\n'));
|
|
335
|
+
console.log(chalk.blue('Please install tools manually:\n'));
|
|
336
|
+
for (const tool of missingTools) {
|
|
337
|
+
console.log(chalk.white(` ${tool}:`));
|
|
338
|
+
this.printInstallInstructions(tool);
|
|
339
|
+
console.log('');
|
|
340
|
+
}
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
else {
|
|
345
|
+
console.log(chalk.yellow('\nCannot proceed with automatic installation without a package manager.\n'));
|
|
346
|
+
console.log(chalk.blue('Please install tools manually:\n'));
|
|
347
|
+
for (const tool of missingTools) {
|
|
348
|
+
console.log(chalk.white(` ${tool}:`));
|
|
349
|
+
this.printInstallInstructions(tool);
|
|
350
|
+
console.log('');
|
|
351
|
+
}
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
if (packageManager) {
|
|
356
|
+
console.log(chalk.gray(`Using package manager: ${chalk.cyan(packageManager)}\n`));
|
|
357
|
+
}
|
|
358
|
+
for (const tool of missingTools) {
|
|
359
|
+
const { installNow } = await inquirer_1.default.prompt([
|
|
360
|
+
{
|
|
361
|
+
type: 'confirm',
|
|
362
|
+
name: 'installNow',
|
|
363
|
+
message: `Install ${tool} now?`,
|
|
364
|
+
default: true,
|
|
365
|
+
},
|
|
366
|
+
]);
|
|
367
|
+
if (installNow) {
|
|
368
|
+
try {
|
|
369
|
+
await this.installTool(tool, packageManager);
|
|
370
|
+
console.log(chalk.green(`\nā ${tool} installed successfully!\n`));
|
|
371
|
+
}
|
|
372
|
+
catch (error) {
|
|
373
|
+
console.log(chalk.yellow(`\nā ļø Could not install ${tool} automatically.`));
|
|
374
|
+
console.log(chalk.blue('\nPlease install it manually:\n'));
|
|
375
|
+
this.printInstallInstructions(tool);
|
|
376
|
+
console.log('');
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
else {
|
|
380
|
+
console.log(chalk.blue(`\nTo install ${tool} manually:\n`));
|
|
381
|
+
this.printInstallInstructions(tool);
|
|
382
|
+
console.log('');
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
async installTool(tool, packageManager) {
|
|
387
|
+
const platform = process.platform;
|
|
388
|
+
// Package names mapping for different package managers
|
|
389
|
+
const packageNames = {
|
|
390
|
+
kubectl: {
|
|
391
|
+
choco: 'kubernetes-cli',
|
|
392
|
+
winget: 'Kubernetes.kubectl',
|
|
393
|
+
scoop: 'kubectl',
|
|
394
|
+
brew: 'kubectl',
|
|
395
|
+
apt: 'kubectl',
|
|
396
|
+
snap: 'kubectl',
|
|
397
|
+
},
|
|
398
|
+
doctl: {
|
|
399
|
+
choco: 'doctl',
|
|
400
|
+
winget: 'DigitalOcean.Doctl',
|
|
401
|
+
scoop: 'doctl',
|
|
402
|
+
brew: 'doctl',
|
|
403
|
+
apt: 'doctl',
|
|
404
|
+
snap: 'doctl',
|
|
405
|
+
},
|
|
406
|
+
'gh cli': {
|
|
407
|
+
choco: 'gh',
|
|
408
|
+
winget: 'GitHub.cli',
|
|
409
|
+
scoop: 'gh',
|
|
410
|
+
brew: 'gh',
|
|
411
|
+
apt: 'gh',
|
|
412
|
+
snap: 'gh',
|
|
413
|
+
},
|
|
414
|
+
helm: {
|
|
415
|
+
choco: 'kubernetes-helm',
|
|
416
|
+
winget: 'Helm.Helm',
|
|
417
|
+
scoop: 'helm',
|
|
418
|
+
brew: 'helm',
|
|
419
|
+
apt: 'helm',
|
|
420
|
+
snap: 'helm',
|
|
421
|
+
},
|
|
422
|
+
};
|
|
423
|
+
const packageName = packageNames[tool]?.[packageManager];
|
|
424
|
+
if (!packageName) {
|
|
425
|
+
throw new Error(`Unknown tool: ${tool} for package manager: ${packageManager}`);
|
|
426
|
+
}
|
|
427
|
+
if (platform === 'win32') {
|
|
428
|
+
if (packageManager === 'choco') {
|
|
429
|
+
await this.runner.executeCommand(runner_service_1.ProgramName.POWERSHELL, ['-Command', `choco install ${packageName} -y`], {}, false);
|
|
430
|
+
}
|
|
431
|
+
else if (packageManager === 'winget') {
|
|
432
|
+
await this.runner.executeCommand(runner_service_1.ProgramName.POWERSHELL, [
|
|
433
|
+
'-Command',
|
|
434
|
+
`winget install --id ${packageName} --silent --accept-package-agreements --accept-source-agreements`,
|
|
435
|
+
], {}, false);
|
|
436
|
+
}
|
|
437
|
+
else if (packageManager === 'scoop') {
|
|
438
|
+
await this.runner.executeCommand(runner_service_1.ProgramName.POWERSHELL, ['-Command', `scoop install ${packageName}`], {}, false);
|
|
439
|
+
}
|
|
440
|
+
else {
|
|
441
|
+
throw new Error(`Unsupported package manager: ${packageManager}`);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
else if (platform === 'darwin') {
|
|
445
|
+
if (packageManager === 'brew') {
|
|
446
|
+
await this.runner.executeCommand(runner_service_1.ProgramName.BREW, ['install', packageName], {}, false);
|
|
447
|
+
}
|
|
448
|
+
else {
|
|
449
|
+
throw new Error(`Unsupported package manager for macOS: ${packageManager}`);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
else if (platform === 'linux') {
|
|
453
|
+
if (packageManager === 'apt') {
|
|
454
|
+
await this.runner.executeCommand(runner_service_1.ProgramName.POWERSHELL, ['-Command', `sudo apt update && sudo apt install -y ${packageName}`], {}, false);
|
|
455
|
+
}
|
|
456
|
+
else if (packageManager === 'snap') {
|
|
457
|
+
await this.runner.executeCommand(runner_service_1.ProgramName.POWERSHELL, ['-Command', `sudo snap install ${packageName} --classic`], {}, false);
|
|
458
|
+
}
|
|
459
|
+
else {
|
|
460
|
+
throw new Error(`Unsupported package manager for Linux: ${packageManager}`);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
else {
|
|
464
|
+
throw new Error('Automatic installation not supported on this platform');
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
printInstallInstructions(tool) {
|
|
468
|
+
const platform = process.platform;
|
|
469
|
+
switch (tool) {
|
|
470
|
+
case 'kubectl':
|
|
471
|
+
console.log(chalk.gray(' Windows (Chocolatey): choco install kubernetes-cli'));
|
|
472
|
+
console.log(chalk.gray(' Windows (Scoop): scoop install kubectl'));
|
|
473
|
+
console.log(chalk.gray(' Windows (winget): winget install Kubernetes.kubectl'));
|
|
474
|
+
console.log(chalk.gray(' macOS (Homebrew): brew install kubectl'));
|
|
475
|
+
console.log(chalk.gray(' Linux (apt): sudo apt install kubectl'));
|
|
476
|
+
console.log(chalk.gray(' Linux (snap): sudo snap install kubectl --classic'));
|
|
477
|
+
break;
|
|
478
|
+
case 'doctl':
|
|
479
|
+
console.log(chalk.gray(' Windows (Chocolatey): choco install doctl'));
|
|
480
|
+
console.log(chalk.gray(' Windows (Scoop): scoop install doctl'));
|
|
481
|
+
console.log(chalk.gray(' Windows (winget): winget install DigitalOcean.Doctl'));
|
|
482
|
+
console.log(chalk.gray(' macOS (Homebrew): brew install doctl'));
|
|
483
|
+
console.log(chalk.gray(' Linux (snap): sudo snap install doctl'));
|
|
484
|
+
console.log(chalk.gray(' Linux (manual): https://docs.digitalocean.com/reference/doctl/how-to/install/'));
|
|
485
|
+
break;
|
|
486
|
+
case 'gh cli':
|
|
487
|
+
console.log(chalk.gray(' Windows (Chocolatey): choco install gh'));
|
|
488
|
+
console.log(chalk.gray(' Windows (Scoop): scoop install gh'));
|
|
489
|
+
console.log(chalk.gray(' Windows (winget): winget install GitHub.cli'));
|
|
490
|
+
console.log(chalk.gray(' macOS (Homebrew): brew install gh'));
|
|
491
|
+
console.log(chalk.gray(' Linux (apt): sudo apt install gh'));
|
|
492
|
+
console.log(chalk.gray(' Linux (snap): sudo snap install gh'));
|
|
493
|
+
break;
|
|
494
|
+
case 'helm':
|
|
495
|
+
console.log(chalk.gray(' Windows (Chocolatey): choco install kubernetes-helm'));
|
|
496
|
+
console.log(chalk.gray(' Windows (Scoop): scoop install helm'));
|
|
497
|
+
console.log(chalk.gray(' Windows (winget): winget install Helm.Helm'));
|
|
498
|
+
console.log(chalk.gray(' macOS (Homebrew): brew install helm'));
|
|
499
|
+
console.log(chalk.gray(' Linux (snap): sudo snap install helm --classic'));
|
|
500
|
+
console.log(chalk.gray(' Linux (manual): https://helm.sh/docs/intro/install/'));
|
|
501
|
+
break;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
async getGitRepoName(path) {
|
|
505
|
+
try {
|
|
506
|
+
const result = await this.runner.executeCommand(runner_service_1.ProgramName.GIT, ['config', '--get', 'remote.origin.url'], { cwd: path }, true);
|
|
507
|
+
const url = result.stdout.trim();
|
|
508
|
+
// Extract repo name from git URL
|
|
509
|
+
const match = url.match(/\/([^\/]+?)(\.git)?$/);
|
|
510
|
+
return match ? match[1] : null;
|
|
511
|
+
}
|
|
512
|
+
catch {
|
|
513
|
+
return null;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
async getCurrentKubeContext() {
|
|
517
|
+
try {
|
|
518
|
+
const result = await this.runner.executeCommand(runner_service_1.ProgramName.KUBECTL, ['config', 'current-context'], {}, true);
|
|
519
|
+
return result.stdout.trim();
|
|
520
|
+
}
|
|
521
|
+
catch {
|
|
522
|
+
return null;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
async getKubeNamespaces() {
|
|
526
|
+
try {
|
|
527
|
+
const result = await this.runner.executeCommand(runner_service_1.ProgramName.KUBECTL, ['get', 'namespaces', '-o', 'jsonpath={.items[*].metadata.name}'], {}, true);
|
|
528
|
+
return result.stdout
|
|
529
|
+
.trim()
|
|
530
|
+
.split(/\s+/)
|
|
531
|
+
.filter((n) => n.length > 0);
|
|
532
|
+
}
|
|
533
|
+
catch {
|
|
534
|
+
return [];
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
async getKubeClusters() {
|
|
538
|
+
try {
|
|
539
|
+
const result = await this.runner.executeCommand(runner_service_1.ProgramName.KUBECTL, ['config', 'get-clusters'], {}, true);
|
|
540
|
+
return result.stdout
|
|
541
|
+
.trim()
|
|
542
|
+
.split('\n')
|
|
543
|
+
.slice(1) // Skip header
|
|
544
|
+
.filter((c) => c.length > 0);
|
|
545
|
+
}
|
|
546
|
+
catch {
|
|
547
|
+
return [];
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
async runDeploymentWizard() {
|
|
551
|
+
console.log(chalk.blue.bold('\nš Deployment Configuration Wizard\n'));
|
|
552
|
+
console.log(chalk.gray('This wizard will help you set up CI/CD for your project.\n'));
|
|
553
|
+
// Gather context information
|
|
554
|
+
const spinner = ora('Gathering environment information...').start();
|
|
555
|
+
const gitRepoName = await this.getGitRepoName(process.cwd());
|
|
556
|
+
const currentDir = pathModule.basename(process.cwd());
|
|
557
|
+
const defaultAppName = gitRepoName || currentDir;
|
|
558
|
+
const currentContext = await this.getCurrentKubeContext();
|
|
559
|
+
const availableNamespaces = await this.getKubeNamespaces();
|
|
560
|
+
const availableClusters = await this.getKubeClusters();
|
|
561
|
+
spinner.stop();
|
|
562
|
+
if (currentContext) {
|
|
563
|
+
console.log(chalk.gray(`Current kubectl context: ${chalk.cyan(currentContext)}\n`));
|
|
564
|
+
}
|
|
565
|
+
if (availableNamespaces.length > 0) {
|
|
566
|
+
console.log(chalk.gray(`Available namespaces: ${chalk.cyan(availableNamespaces.join(', '))}\n`));
|
|
567
|
+
}
|
|
568
|
+
const answers = await inquirer_1.default.prompt([
|
|
569
|
+
{
|
|
570
|
+
type: 'list',
|
|
571
|
+
name: 'provider',
|
|
572
|
+
message: 'Select your Kubernetes provider:',
|
|
573
|
+
choices: [
|
|
574
|
+
{ name: 'Digital Ocean Kubernetes', value: 'digitalocean' },
|
|
575
|
+
{ name: 'Other (Coming soon)', value: 'other', disabled: true },
|
|
576
|
+
],
|
|
577
|
+
default: 'digitalocean',
|
|
578
|
+
},
|
|
579
|
+
{
|
|
580
|
+
type: 'list',
|
|
581
|
+
name: 'cicd',
|
|
582
|
+
message: 'Select your CI/CD platform:',
|
|
583
|
+
choices: [
|
|
584
|
+
{ name: 'GitHub Actions', value: 'github-actions' },
|
|
585
|
+
{ name: 'Other (Coming soon)', value: 'other', disabled: true },
|
|
586
|
+
],
|
|
587
|
+
default: 'github-actions',
|
|
588
|
+
},
|
|
589
|
+
{
|
|
590
|
+
type: 'list',
|
|
591
|
+
name: 'clusterSelection',
|
|
592
|
+
message: 'How would you like to specify your cluster?',
|
|
593
|
+
choices: availableClusters.length > 0
|
|
594
|
+
? [
|
|
595
|
+
{ name: 'Use current context', value: 'current' },
|
|
596
|
+
{ name: 'Select from available clusters', value: 'select' },
|
|
597
|
+
{ name: 'Enter cluster name manually', value: 'manual' },
|
|
598
|
+
]
|
|
599
|
+
: [{ name: 'Enter cluster name manually', value: 'manual' }],
|
|
600
|
+
default: availableClusters.length > 0 ? 'current' : 'manual',
|
|
601
|
+
when: () => availableClusters.length > 0 || !currentContext,
|
|
602
|
+
},
|
|
603
|
+
{
|
|
604
|
+
type: 'list',
|
|
605
|
+
name: 'clusterName',
|
|
606
|
+
message: 'Select your Kubernetes cluster:',
|
|
607
|
+
choices: availableClusters,
|
|
608
|
+
when: (answers) => answers.clusterSelection === 'select',
|
|
609
|
+
},
|
|
610
|
+
{
|
|
611
|
+
type: 'input',
|
|
612
|
+
name: 'clusterName',
|
|
613
|
+
message: 'Enter your Kubernetes cluster name:',
|
|
614
|
+
validate: (input) => input.length > 0 || 'Cluster name is required',
|
|
615
|
+
when: (answers) => answers.clusterSelection === 'manual',
|
|
616
|
+
},
|
|
617
|
+
{
|
|
618
|
+
type: 'list',
|
|
619
|
+
name: 'namespaceSelection',
|
|
620
|
+
message: 'How would you like to configure the namespace?',
|
|
621
|
+
choices: availableNamespaces.length > 0
|
|
622
|
+
? [
|
|
623
|
+
{ name: 'Use existing namespace', value: 'existing' },
|
|
624
|
+
{ name: 'Create new namespace', value: 'new' },
|
|
625
|
+
]
|
|
626
|
+
: [{ name: 'Create new namespace', value: 'new' }],
|
|
627
|
+
default: availableNamespaces.length > 0 ? 'existing' : 'new',
|
|
628
|
+
},
|
|
629
|
+
{
|
|
630
|
+
type: 'list',
|
|
631
|
+
name: 'namespace',
|
|
632
|
+
message: 'Select an existing namespace:',
|
|
633
|
+
choices: availableNamespaces,
|
|
634
|
+
default: availableNamespaces.includes('production')
|
|
635
|
+
? 'production'
|
|
636
|
+
: availableNamespaces[0],
|
|
637
|
+
when: (answers) => answers.namespaceSelection === 'existing' &&
|
|
638
|
+
availableNamespaces.length > 0,
|
|
639
|
+
},
|
|
640
|
+
{
|
|
641
|
+
type: 'input',
|
|
642
|
+
name: 'namespace',
|
|
643
|
+
message: 'Enter the new namespace name:',
|
|
644
|
+
default: 'production',
|
|
645
|
+
validate: (input) => {
|
|
646
|
+
if (input.length === 0)
|
|
647
|
+
return 'Namespace name is required';
|
|
648
|
+
if (!/^[a-z0-9]([-a-z0-9]*[a-z0-9])?$/.test(input)) {
|
|
649
|
+
return 'Namespace must be lowercase alphanumeric and may contain hyphens';
|
|
650
|
+
}
|
|
651
|
+
return true;
|
|
652
|
+
},
|
|
653
|
+
when: (answers) => answers.namespaceSelection === 'new',
|
|
654
|
+
},
|
|
655
|
+
{
|
|
656
|
+
type: 'input',
|
|
657
|
+
name: 'appName',
|
|
658
|
+
message: 'Enter your application name:',
|
|
659
|
+
default: defaultAppName,
|
|
660
|
+
validate: (input) => {
|
|
661
|
+
if (input.length === 0)
|
|
662
|
+
return 'Application name is required';
|
|
663
|
+
if (!/^[a-z0-9]([-a-z0-9]*[a-z0-9])?$/.test(input)) {
|
|
664
|
+
return 'Application name must be lowercase alphanumeric and may contain hyphens';
|
|
665
|
+
}
|
|
666
|
+
return true;
|
|
667
|
+
},
|
|
668
|
+
},
|
|
669
|
+
{
|
|
670
|
+
type: 'input',
|
|
671
|
+
name: 'containerRegistry',
|
|
672
|
+
message: 'Enter your container registry:',
|
|
673
|
+
default: (answers) => `registry.digitalocean.com/${answers.appName}`,
|
|
674
|
+
validate: (input) => input.length > 0 || 'Container registry is required',
|
|
675
|
+
},
|
|
676
|
+
{
|
|
677
|
+
type: 'input',
|
|
678
|
+
name: 'domain',
|
|
679
|
+
message: 'Enter your domain (optional, press Enter to skip):',
|
|
680
|
+
},
|
|
681
|
+
{
|
|
682
|
+
type: 'checkbox',
|
|
683
|
+
name: 'apps',
|
|
684
|
+
message: 'Select which apps to deploy:',
|
|
685
|
+
choices: [
|
|
686
|
+
{ name: 'API (Backend)', value: 'api', checked: true },
|
|
687
|
+
{ name: 'Admin (Frontend)', value: 'admin', checked: true },
|
|
688
|
+
],
|
|
689
|
+
},
|
|
690
|
+
{
|
|
691
|
+
type: 'confirm',
|
|
692
|
+
name: 'setupIngress',
|
|
693
|
+
message: 'Would you like to set up Ingress for external access?',
|
|
694
|
+
default: true,
|
|
695
|
+
when: (answers) => answers.domain && answers.domain.length > 0,
|
|
696
|
+
},
|
|
697
|
+
{
|
|
698
|
+
type: 'confirm',
|
|
699
|
+
name: 'setupSSL',
|
|
700
|
+
message: 'Would you like to set up SSL/TLS with cert-manager?',
|
|
701
|
+
default: true,
|
|
702
|
+
when: (answers) => answers.setupIngress,
|
|
703
|
+
},
|
|
704
|
+
{
|
|
705
|
+
type: 'input',
|
|
706
|
+
name: 'email',
|
|
707
|
+
message: 'Enter your email for SSL certificate notifications:',
|
|
708
|
+
validate: (input) => {
|
|
709
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
710
|
+
return emailRegex.test(input) || 'Please enter a valid email address';
|
|
711
|
+
},
|
|
712
|
+
when: (answers) => answers.setupSSL,
|
|
713
|
+
},
|
|
714
|
+
{
|
|
715
|
+
type: 'confirm',
|
|
716
|
+
name: 'confirmGeneration',
|
|
717
|
+
message: 'Generate deployment configuration files now?',
|
|
718
|
+
default: true,
|
|
719
|
+
},
|
|
720
|
+
]);
|
|
721
|
+
if (!answers.confirmGeneration) {
|
|
722
|
+
return null;
|
|
723
|
+
}
|
|
724
|
+
// Set cluster name from current context if using current
|
|
725
|
+
if (answers.clusterSelection === 'current' && currentContext) {
|
|
726
|
+
// Extract cluster name from context (format may vary)
|
|
727
|
+
answers.clusterName = currentContext;
|
|
728
|
+
}
|
|
729
|
+
// Store if namespace needs to be created
|
|
730
|
+
const createNamespace = answers.namespaceSelection === 'new';
|
|
731
|
+
return {
|
|
732
|
+
...answers,
|
|
733
|
+
createNamespace,
|
|
734
|
+
};
|
|
735
|
+
}
|
|
736
|
+
async generateDeploymentFiles(path, config) {
|
|
737
|
+
// Create .github/workflows directory
|
|
738
|
+
const workflowsDir = pathModule.join(path, '.github', 'workflows');
|
|
739
|
+
await (0, promises_1.mkdir)(workflowsDir, { recursive: true });
|
|
740
|
+
// Create k8s directory
|
|
741
|
+
const k8sDir = pathModule.join(path, 'k8s');
|
|
742
|
+
await (0, promises_1.mkdir)(k8sDir, { recursive: true });
|
|
743
|
+
// Generate Dockerfiles and .dockerignore for each app
|
|
744
|
+
for (const app of config.apps) {
|
|
745
|
+
await this.generateDockerfile(path, app, config);
|
|
746
|
+
}
|
|
747
|
+
// Generate .dockerignore in root
|
|
748
|
+
await this.generateDockerignore(path);
|
|
749
|
+
// Generate GitHub Actions workflow
|
|
750
|
+
await this.generateGitHubActionsWorkflow(workflowsDir, config);
|
|
751
|
+
// Generate Kubernetes manifests for each app
|
|
752
|
+
for (const app of config.apps) {
|
|
753
|
+
await this.generateKubernetesManifests(k8sDir, app, config);
|
|
754
|
+
}
|
|
755
|
+
// Generate Ingress if requested
|
|
756
|
+
if (config.setupIngress) {
|
|
757
|
+
await this.generateIngressManifest(k8sDir, config);
|
|
758
|
+
}
|
|
759
|
+
// Generate cert-manager ClusterIssuer if SSL is requested
|
|
760
|
+
if (config.setupSSL) {
|
|
761
|
+
await this.generateCertManagerIssuer(k8sDir, config);
|
|
762
|
+
}
|
|
763
|
+
// Generate Helm chart configuration (optional)
|
|
764
|
+
await this.generateHelmChart(path, config);
|
|
765
|
+
// Generate README with deployment instructions
|
|
766
|
+
await this.generateDeploymentReadme(path, config);
|
|
767
|
+
}
|
|
768
|
+
async generateDockerfile(path, app, config) {
|
|
769
|
+
const templatePath = pathModule.join(__dirname, '..', '..', 'templates', 'deployment', `${app}.Dockerfile.ejs`);
|
|
770
|
+
const dockerfilePath = pathModule.join(path, 'apps', app, 'Dockerfile');
|
|
771
|
+
// Check if Dockerfile already exists
|
|
772
|
+
if ((0, fs_1.existsSync)(dockerfilePath)) {
|
|
773
|
+
this.log(chalk.yellow(`Dockerfile already exists for ${app}, skipping...`));
|
|
774
|
+
return;
|
|
775
|
+
}
|
|
776
|
+
try {
|
|
777
|
+
const templateContent = await (0, promises_1.readFile)(templatePath, 'utf8');
|
|
778
|
+
const renderedContent = await (0, ejs_1.render)(templateContent, { config, app });
|
|
779
|
+
await (0, promises_1.writeFile)(dockerfilePath, renderedContent, 'utf8');
|
|
780
|
+
this.log(chalk.green(`Created Dockerfile for ${app}`));
|
|
781
|
+
}
|
|
782
|
+
catch (error) {
|
|
783
|
+
this.log(chalk.yellow(`Could not create Dockerfile from template for ${app}, creating basic version...`));
|
|
784
|
+
// Create a basic Dockerfile if template doesn't exist
|
|
785
|
+
const basicDockerfile = this.generateBasicDockerfile(app);
|
|
786
|
+
await (0, promises_1.writeFile)(dockerfilePath, basicDockerfile, 'utf8');
|
|
787
|
+
this.log(chalk.green(`Created basic Dockerfile for ${app}`));
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
generateBasicDockerfile(app) {
|
|
791
|
+
if (app === 'api') {
|
|
792
|
+
return `# Dockerfile for API
|
|
793
|
+
FROM node:18-alpine AS builder
|
|
794
|
+
WORKDIR /app
|
|
795
|
+
COPY package.json pnpm-lock.yaml ./
|
|
796
|
+
COPY apps/api/package.json ./apps/api/
|
|
797
|
+
RUN npm install -g pnpm
|
|
798
|
+
RUN pnpm install --frozen-lockfile
|
|
799
|
+
COPY . .
|
|
800
|
+
RUN pnpm --filter api build
|
|
801
|
+
|
|
802
|
+
FROM node:18-alpine
|
|
803
|
+
WORKDIR /app
|
|
804
|
+
COPY package.json pnpm-lock.yaml ./
|
|
805
|
+
COPY apps/api/package.json ./apps/api/
|
|
806
|
+
RUN npm install -g pnpm
|
|
807
|
+
RUN pnpm install --frozen-lockfile --prod
|
|
808
|
+
COPY --from=builder /app/apps/api/dist ./apps/api/dist
|
|
809
|
+
ENV NODE_ENV=production
|
|
810
|
+
ENV PORT=3000
|
|
811
|
+
EXPOSE 3000
|
|
812
|
+
CMD ["node", "apps/api/dist/main.js"]
|
|
813
|
+
`;
|
|
814
|
+
}
|
|
815
|
+
else if (app === 'admin') {
|
|
816
|
+
return `# Dockerfile for Admin
|
|
817
|
+
FROM node:18-alpine AS builder
|
|
818
|
+
WORKDIR /app
|
|
819
|
+
COPY package.json pnpm-lock.yaml ./
|
|
820
|
+
COPY apps/admin/package.json ./apps/admin/
|
|
821
|
+
RUN npm install -g pnpm
|
|
822
|
+
RUN pnpm install --frozen-lockfile
|
|
823
|
+
COPY . .
|
|
824
|
+
RUN pnpm --filter admin build
|
|
825
|
+
|
|
826
|
+
FROM node:18-alpine
|
|
827
|
+
WORKDIR /app
|
|
828
|
+
RUN npm install -g pnpm
|
|
829
|
+
COPY --from=builder /app/apps/admin/.next ./apps/admin/.next
|
|
830
|
+
COPY --from=builder /app/apps/admin/public ./apps/admin/public
|
|
831
|
+
COPY --from=builder /app/apps/admin/package.json ./apps/admin/
|
|
832
|
+
COPY --from=builder /app/apps/admin/node_modules ./apps/admin/node_modules
|
|
833
|
+
ENV NODE_ENV=production
|
|
834
|
+
ENV PORT=80
|
|
835
|
+
EXPOSE 80
|
|
836
|
+
WORKDIR /app/apps/admin
|
|
837
|
+
CMD ["pnpm", "start"]
|
|
838
|
+
`;
|
|
839
|
+
}
|
|
840
|
+
return `# Dockerfile for ${app}
|
|
841
|
+
FROM node:18-alpine
|
|
842
|
+
WORKDIR /app
|
|
843
|
+
COPY . .
|
|
844
|
+
RUN npm install -g pnpm
|
|
845
|
+
RUN pnpm install --frozen-lockfile
|
|
846
|
+
RUN pnpm build
|
|
847
|
+
EXPOSE 3000
|
|
848
|
+
CMD ["pnpm", "start"]
|
|
849
|
+
`;
|
|
850
|
+
}
|
|
851
|
+
async generateDockerignore(path) {
|
|
852
|
+
const dockerignorePath = pathModule.join(path, '.dockerignore');
|
|
853
|
+
// Check if .dockerignore already exists
|
|
854
|
+
if ((0, fs_1.existsSync)(dockerignorePath)) {
|
|
855
|
+
this.log(chalk.yellow('.dockerignore already exists, skipping...'));
|
|
856
|
+
return;
|
|
857
|
+
}
|
|
858
|
+
const templatePath = pathModule.join(__dirname, '..', '..', 'templates', 'deployment', '.dockerignore.ejs');
|
|
859
|
+
try {
|
|
860
|
+
const templateContent = await (0, promises_1.readFile)(templatePath, 'utf8');
|
|
861
|
+
await (0, promises_1.writeFile)(dockerignorePath, templateContent, 'utf8');
|
|
862
|
+
this.log(chalk.green('Created .dockerignore'));
|
|
863
|
+
}
|
|
864
|
+
catch (error) {
|
|
865
|
+
// Create basic .dockerignore if template doesn't exist
|
|
866
|
+
const basicDockerignore = `node_modules
|
|
867
|
+
dist
|
|
868
|
+
build
|
|
869
|
+
.next
|
|
870
|
+
.env
|
|
871
|
+
.env.local
|
|
872
|
+
.git
|
|
873
|
+
.github
|
|
874
|
+
k8s
|
|
875
|
+
helm
|
|
876
|
+
*.md
|
|
877
|
+
test
|
|
878
|
+
tests
|
|
879
|
+
coverage
|
|
880
|
+
.vscode
|
|
881
|
+
.idea
|
|
882
|
+
*.log
|
|
883
|
+
tmp
|
|
884
|
+
temp
|
|
885
|
+
`;
|
|
886
|
+
await (0, promises_1.writeFile)(dockerignorePath, basicDockerignore, 'utf8');
|
|
887
|
+
this.log(chalk.green('Created basic .dockerignore'));
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
async generateGitHubActionsWorkflow(dir, config) {
|
|
891
|
+
const workflowContent = `name: Deploy to Kubernetes
|
|
892
|
+
|
|
893
|
+
onon:
|
|
894
|
+
push:
|
|
895
|
+
branches:
|
|
896
|
+
- main
|
|
897
|
+
- production
|
|
898
|
+
workflow_dispatch:
|
|
899
|
+
|
|
900
|
+
env:
|
|
901
|
+
REGISTRY: ${config.containerRegistry}
|
|
902
|
+
CLUSTER_NAME: ${config.clusterName}
|
|
903
|
+
NAMESPACE: ${config.namespace}
|
|
904
|
+
|
|
905
|
+
jobs:
|
|
906
|
+
${config.apps
|
|
907
|
+
.map((app) => ` deploy-${app}:
|
|
908
|
+
name: Deploy ${app.toUpperCase()}
|
|
909
|
+
runs-on: ubuntu-latest
|
|
910
|
+
|
|
911
|
+
steps:
|
|
912
|
+
- name: Checkout code
|
|
913
|
+
uses: actions/checkout@v4
|
|
914
|
+
|
|
915
|
+
- name: Install doctl
|
|
916
|
+
uses: digitalocean/action-doctl@v2
|
|
917
|
+
with:
|
|
918
|
+
token: \${{ secrets.DIGITALOCEAN_ACCESS_TOKEN }}
|
|
919
|
+
|
|
920
|
+
- name: Log in to Container Registry
|
|
921
|
+
run: doctl registry login
|
|
922
|
+
|
|
923
|
+
- name: Build and push Docker image
|
|
924
|
+
run: |
|
|
925
|
+
docker build -t \${{ env.REGISTRY }}/${config.appName}-${app}:\${{ github.sha }} \\
|
|
926
|
+
-f apps/${app}/Dockerfile .
|
|
927
|
+
docker push \${{ env.REGISTRY }}/${config.appName}-${app}:\${{ github.sha }}
|
|
928
|
+
docker tag \${{ env.REGISTRY }}/${config.appName}-${app}:\${{ github.sha }} \\
|
|
929
|
+
\${{ env.REGISTRY }}/${config.appName}-${app}:latest
|
|
930
|
+
docker push \${{ env.REGISTRY }}/${config.appName}-${app}:latest
|
|
931
|
+
|
|
932
|
+
- name: Save DigitalOcean kubeconfig
|
|
933
|
+
run: doctl kubernetes cluster kubeconfig save \${{ env.CLUSTER_NAME }}
|
|
934
|
+
|
|
935
|
+
- name: Deploy to Kubernetes
|
|
936
|
+
run: |
|
|
937
|
+
kubectl set image deployment/${config.appName}-${app} \\
|
|
938
|
+
${config.appName}-${app}=\${{ env.REGISTRY }}/${config.appName}-${app}:\${{ github.sha }} \\
|
|
939
|
+
-n \${{ env.NAMESPACE }}
|
|
940
|
+
kubectl rollout status deployment/${config.appName}-${app} -n \${{ env.NAMESPACE }}
|
|
941
|
+
`)
|
|
942
|
+
.join('\n')}
|
|
943
|
+
`;
|
|
944
|
+
const workflowPath = pathModule.join(dir, 'deploy.yml');
|
|
945
|
+
await (0, promises_1.writeFile)(workflowPath, workflowContent, 'utf8');
|
|
946
|
+
this.log(chalk.green(`Created GitHub Actions workflow: ${workflowPath}`));
|
|
947
|
+
}
|
|
948
|
+
async generateKubernetesManifests(dir, app, config) {
|
|
949
|
+
const appDir = pathModule.join(dir, app);
|
|
950
|
+
await (0, promises_1.mkdir)(appDir, { recursive: true });
|
|
951
|
+
// Generate Deployment
|
|
952
|
+
const deploymentContent = `apiVersion: apps/v1
|
|
953
|
+
kind: Deployment
|
|
954
|
+
metadata:
|
|
955
|
+
name: ${config.appName}-${app}
|
|
956
|
+
namespace: ${config.namespace}
|
|
957
|
+
labels:
|
|
958
|
+
app: ${config.appName}-${app}
|
|
959
|
+
spec:
|
|
960
|
+
replicas: 2
|
|
961
|
+
selector:
|
|
962
|
+
matchLabels:
|
|
963
|
+
app: ${config.appName}-${app}
|
|
964
|
+
template:
|
|
965
|
+
metadata:
|
|
966
|
+
labels:
|
|
967
|
+
app: ${config.appName}-${app}
|
|
968
|
+
spec:
|
|
969
|
+
containers:
|
|
970
|
+
- name: ${config.appName}-${app}
|
|
971
|
+
image: ${config.containerRegistry}/${config.appName}-${app}:latest
|
|
972
|
+
ports:
|
|
973
|
+
- containerPort: ${app === 'api' ? '3000' : '80'}
|
|
974
|
+
env:
|
|
975
|
+
- name: NODE_ENV
|
|
976
|
+
value: "production"
|
|
977
|
+
resources:
|
|
978
|
+
requests:
|
|
979
|
+
memory: "256Mi"
|
|
980
|
+
cpu: "100m"
|
|
981
|
+
limits:
|
|
982
|
+
memory: "512Mi"
|
|
983
|
+
cpu: "500m"
|
|
984
|
+
livenessProbe:
|
|
985
|
+
httpGet:
|
|
986
|
+
path: ${app === 'api' ? '/health' : '/'}
|
|
987
|
+
port: ${app === 'api' ? '3000' : '80'}
|
|
988
|
+
initialDelaySeconds: 30
|
|
989
|
+
periodSeconds: 10
|
|
990
|
+
readinessProbe:
|
|
991
|
+
httpGet:
|
|
992
|
+
path: ${app === 'api' ? '/health' : '/'}
|
|
993
|
+
port: ${app === 'api' ? '3000' : '80'}
|
|
994
|
+
initialDelaySeconds: 10
|
|
995
|
+
periodSeconds: 5
|
|
996
|
+
`;
|
|
997
|
+
await (0, promises_1.writeFile)(pathModule.join(appDir, 'deployment.yaml'), deploymentContent, 'utf8');
|
|
998
|
+
// Generate Service
|
|
999
|
+
const serviceContent = `apiVersion: v1
|
|
1000
|
+
kind: Service
|
|
1001
|
+
metadata:
|
|
1002
|
+
name: ${config.appName}-${app}
|
|
1003
|
+
namespace: ${config.namespace}
|
|
1004
|
+
labels:
|
|
1005
|
+
app: ${config.appName}-${app}
|
|
1006
|
+
spec:
|
|
1007
|
+
type: ClusterIP
|
|
1008
|
+
ports:
|
|
1009
|
+
- port: ${app === 'api' ? '3000' : '80'}
|
|
1010
|
+
targetPort: ${app === 'api' ? '3000' : '80'}
|
|
1011
|
+
protocol: TCP
|
|
1012
|
+
name: http
|
|
1013
|
+
selector:
|
|
1014
|
+
app: ${config.appName}-${app}
|
|
1015
|
+
`;
|
|
1016
|
+
await (0, promises_1.writeFile)(pathModule.join(appDir, 'service.yaml'), serviceContent, 'utf8');
|
|
1017
|
+
this.log(chalk.green(`Created Kubernetes manifests for ${app}`));
|
|
1018
|
+
}
|
|
1019
|
+
async generateIngressManifest(dir, config) {
|
|
1020
|
+
const ingressContent = `apiVersion: networking.k8s.io/v1
|
|
1021
|
+
kind: Ingress
|
|
1022
|
+
metadata:
|
|
1023
|
+
name: ${config.appName}-ingress
|
|
1024
|
+
namespace: ${config.namespace}
|
|
1025
|
+
annotations:
|
|
1026
|
+
kubernetes.io/ingress.class: "nginx"
|
|
1027
|
+
${config.setupSSL ? ` cert-manager.io/cluster-issuer: "letsencrypt-prod"` : ''}
|
|
1028
|
+
spec:
|
|
1029
|
+
${config.setupSSL
|
|
1030
|
+
? ` tls:
|
|
1031
|
+
- hosts:
|
|
1032
|
+
- ${config.domain}
|
|
1033
|
+
${config.apps.includes('api') ? ` - api.${config.domain}` : ''}
|
|
1034
|
+
secretName: ${config.appName}-tls
|
|
1035
|
+
`
|
|
1036
|
+
: ''} rules:
|
|
1037
|
+
${config.apps.includes('admin')
|
|
1038
|
+
? ` - host: ${config.domain}
|
|
1039
|
+
http:
|
|
1040
|
+
paths:
|
|
1041
|
+
- path: /
|
|
1042
|
+
pathType: Prefix
|
|
1043
|
+
backend:
|
|
1044
|
+
service:
|
|
1045
|
+
name: ${config.appName}-admin
|
|
1046
|
+
port:
|
|
1047
|
+
number: 80
|
|
1048
|
+
`
|
|
1049
|
+
: ''}${config.apps.includes('api')
|
|
1050
|
+
? ` - host: api.${config.domain}
|
|
1051
|
+
http:
|
|
1052
|
+
paths:
|
|
1053
|
+
- path: /
|
|
1054
|
+
pathType: Prefix
|
|
1055
|
+
backend:
|
|
1056
|
+
service:
|
|
1057
|
+
name: ${config.appName}-api
|
|
1058
|
+
port:
|
|
1059
|
+
number: 3000
|
|
1060
|
+
`
|
|
1061
|
+
: ''}`;
|
|
1062
|
+
await (0, promises_1.writeFile)(pathModule.join(dir, 'ingress.yaml'), ingressContent, 'utf8');
|
|
1063
|
+
this.log(chalk.green('Created Ingress manifest'));
|
|
1064
|
+
}
|
|
1065
|
+
async generateCertManagerIssuer(dir, config) {
|
|
1066
|
+
const issuerContent = `apiVersion: cert-manager.io/v1
|
|
1067
|
+
kind: ClusterIssuer
|
|
1068
|
+
metadata:
|
|
1069
|
+
name: letsencrypt-prod
|
|
1070
|
+
spec:
|
|
1071
|
+
acme:
|
|
1072
|
+
server: https://acme-v02.api.letsencrypt.org/directory
|
|
1073
|
+
email: ${config.email}
|
|
1074
|
+
privateKeySecretRef:
|
|
1075
|
+
name: letsencrypt-prod
|
|
1076
|
+
solvers:
|
|
1077
|
+
- http01:
|
|
1078
|
+
ingress:
|
|
1079
|
+
class: nginx
|
|
1080
|
+
`;
|
|
1081
|
+
await (0, promises_1.writeFile)(pathModule.join(dir, 'cert-manager-issuer.yaml'), issuerContent, 'utf8');
|
|
1082
|
+
this.log(chalk.green('Created cert-manager ClusterIssuer'));
|
|
1083
|
+
}
|
|
1084
|
+
async generateHelmChart(path, config) {
|
|
1085
|
+
const helmDir = pathModule.join(path, 'helm', config.appName);
|
|
1086
|
+
await (0, promises_1.mkdir)(helmDir, { recursive: true });
|
|
1087
|
+
// Generate Chart.yaml
|
|
1088
|
+
const chartContent = `apiVersion: v2
|
|
1089
|
+
name: ${config.appName}
|
|
1090
|
+
description: Helm chart for ${config.appName}
|
|
1091
|
+
type: application
|
|
1092
|
+
version: 1.0.0
|
|
1093
|
+
appVersion: "1.0.0"
|
|
1094
|
+
`;
|
|
1095
|
+
await (0, promises_1.writeFile)(pathModule.join(helmDir, 'Chart.yaml'), chartContent, 'utf8');
|
|
1096
|
+
// Generate values.yaml
|
|
1097
|
+
const valuesContent = `# Default values for ${config.appName}
|
|
1098
|
+
namespace: ${config.namespace}
|
|
1099
|
+
|
|
1100
|
+
registry: ${config.containerRegistry}
|
|
1101
|
+
|
|
1102
|
+
apps:
|
|
1103
|
+
${config.apps
|
|
1104
|
+
.map((app) => ` ${app}:
|
|
1105
|
+
enabled: true
|
|
1106
|
+
replicas: 2
|
|
1107
|
+
image:
|
|
1108
|
+
repository: \${{ .Values.registry }}/${config.appName}-${app}
|
|
1109
|
+
tag: latest
|
|
1110
|
+
pullPolicy: Always
|
|
1111
|
+
service:
|
|
1112
|
+
type: ClusterIP
|
|
1113
|
+
port: ${app === 'api' ? '3000' : '80'}
|
|
1114
|
+
resources:
|
|
1115
|
+
requests:
|
|
1116
|
+
memory: "256Mi"
|
|
1117
|
+
cpu: "100m"
|
|
1118
|
+
limits:
|
|
1119
|
+
memory: "512Mi"
|
|
1120
|
+
cpu: "500m"
|
|
1121
|
+
`)
|
|
1122
|
+
.join('')}
|
|
1123
|
+
${config.setupIngress
|
|
1124
|
+
? `ingress:
|
|
1125
|
+
enabled: true
|
|
1126
|
+
className: nginx
|
|
1127
|
+
annotations:
|
|
1128
|
+
${config.setupSSL ? ` cert-manager.io/cluster-issuer: letsencrypt-prod` : ''}
|
|
1129
|
+
hosts:
|
|
1130
|
+
${config.apps.includes('admin')
|
|
1131
|
+
? ` - host: ${config.domain}
|
|
1132
|
+
paths:
|
|
1133
|
+
- path: /
|
|
1134
|
+
pathType: Prefix
|
|
1135
|
+
backend:
|
|
1136
|
+
service:
|
|
1137
|
+
name: ${config.appName}-admin
|
|
1138
|
+
port:
|
|
1139
|
+
number: 80
|
|
1140
|
+
`
|
|
1141
|
+
: ''}${config.apps.includes('api')
|
|
1142
|
+
? ` - host: api.${config.domain}
|
|
1143
|
+
paths:
|
|
1144
|
+
- path: /
|
|
1145
|
+
pathType: Prefix
|
|
1146
|
+
backend:
|
|
1147
|
+
service:
|
|
1148
|
+
name: ${config.appName}-api
|
|
1149
|
+
port:
|
|
1150
|
+
number: 3000
|
|
1151
|
+
`
|
|
1152
|
+
: ''}${config.setupSSL
|
|
1153
|
+
? ` tls:
|
|
1154
|
+
- secretName: ${config.appName}-tls
|
|
1155
|
+
hosts:
|
|
1156
|
+
- ${config.domain}
|
|
1157
|
+
${config.apps.includes('api') ? ` - api.${config.domain}` : ''}`
|
|
1158
|
+
: ''}`
|
|
1159
|
+
: ''}
|
|
1160
|
+
`;
|
|
1161
|
+
await (0, promises_1.writeFile)(pathModule.join(helmDir, 'values.yaml'), valuesContent, 'utf8');
|
|
1162
|
+
this.log(chalk.green('Created Helm chart'));
|
|
1163
|
+
}
|
|
1164
|
+
async generateDeploymentReadme(path, config) {
|
|
1165
|
+
const readmeContent = `# Deployment Guide
|
|
1166
|
+
|
|
1167
|
+
This project is configured for deployment to **${config.provider === 'digitalocean' ? 'Digital Ocean Kubernetes' : config.provider}** using **${config.cicd === 'github-actions' ? 'GitHub Actions' : config.cicd}**.
|
|
1168
|
+
|
|
1169
|
+
## Prerequisites
|
|
1170
|
+
|
|
1171
|
+
Make sure you have the following tools installed:
|
|
1172
|
+
|
|
1173
|
+
- \`kubectl\` - Kubernetes CLI
|
|
1174
|
+
- \`doctl\` - Digital Ocean CLI
|
|
1175
|
+
- \`gh\` - GitHub CLI
|
|
1176
|
+
- \`helm\` - Kubernetes package manager
|
|
1177
|
+
|
|
1178
|
+
## Initial Setup
|
|
1179
|
+
|
|
1180
|
+
### 1. Configure Digital Ocean
|
|
1181
|
+
|
|
1182
|
+
\`\`\`bash
|
|
1183
|
+
# Authenticate with Digital Ocean
|
|
1184
|
+
doctl auth init
|
|
1185
|
+
|
|
1186
|
+
# Get your cluster kubeconfig
|
|
1187
|
+
doctl kubernetes cluster kubeconfig save ${config.clusterName}
|
|
1188
|
+
\`\`\`
|
|
1189
|
+
|
|
1190
|
+
${config.createNamespace
|
|
1191
|
+
? `### 2. Create Kubernetes Namespace
|
|
1192
|
+
|
|
1193
|
+
\`\`\`bash
|
|
1194
|
+
kubectl create namespace ${config.namespace}
|
|
1195
|
+
\`\`\`
|
|
1196
|
+
|
|
1197
|
+
`
|
|
1198
|
+
: `### 2. Verify Namespace
|
|
1199
|
+
|
|
1200
|
+
\`\`\`bash
|
|
1201
|
+
# Verify the namespace exists
|
|
1202
|
+
kubectl get namespace ${config.namespace}
|
|
1203
|
+
\`\`\`
|
|
1204
|
+
|
|
1205
|
+
`}### 3. Configure GitHub Secrets
|
|
1206
|
+
|
|
1207
|
+
Add the following secrets to your GitHub repository:
|
|
1208
|
+
|
|
1209
|
+
1. \`DIGITALOCEAN_ACCESS_TOKEN\` - Your Digital Ocean API token
|
|
1210
|
+
|
|
1211
|
+
\`\`\`bash
|
|
1212
|
+
# Get your DO token and add it to GitHub
|
|
1213
|
+
gh secret set DIGITALOCEAN_ACCESS_TOKEN
|
|
1214
|
+
\`\`\`
|
|
1215
|
+
|
|
1216
|
+
${config.setupSSL
|
|
1217
|
+
? `### 4. Install cert-manager
|
|
1218
|
+
|
|
1219
|
+
\`\`\`bash
|
|
1220
|
+
# Install cert-manager for SSL certificates
|
|
1221
|
+
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.13.0/cert-manager.yaml
|
|
1222
|
+
|
|
1223
|
+
# Wait for cert-manager to be ready
|
|
1224
|
+
kubectl wait --for=condition=ready pod -l app.kubernetes.io/instance=cert-manager -n cert-manager --timeout=300s
|
|
1225
|
+
|
|
1226
|
+
# Apply the ClusterIssuer
|
|
1227
|
+
kubectl apply -f k8s/cert-manager-issuer.yaml
|
|
1228
|
+
\`\`\`
|
|
1229
|
+
`
|
|
1230
|
+
: ''}${config.setupIngress
|
|
1231
|
+
? `### ${config.setupSSL ? '5' : '4'}. Install NGINX Ingress Controller
|
|
1232
|
+
|
|
1233
|
+
\`\`\`bash
|
|
1234
|
+
# Install NGINX Ingress Controller
|
|
1235
|
+
helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
|
|
1236
|
+
helm repo update
|
|
1237
|
+
|
|
1238
|
+
helm install nginx-ingress ingress-nginx/ingress-nginx \\
|
|
1239
|
+
--namespace ingress-nginx \\
|
|
1240
|
+
--create-namespace \\
|
|
1241
|
+
--set controller.publishService.enabled=true
|
|
1242
|
+
|
|
1243
|
+
# Wait for the load balancer IP
|
|
1244
|
+
kubectl get service nginx-ingress-ingress-nginx-controller -n ingress-nginx --watch
|
|
1245
|
+
\`\`\`
|
|
1246
|
+
|
|
1247
|
+
**Important:** After the LoadBalancer gets an external IP, configure your DNS:
|
|
1248
|
+
|
|
1249
|
+
${config.domain
|
|
1250
|
+
? `- Point \`${config.domain}\` to the LoadBalancer IP
|
|
1251
|
+
${config.apps.includes('api')
|
|
1252
|
+
? `- Point \`api.${config.domain}\` to the LoadBalancer IP
|
|
1253
|
+
`
|
|
1254
|
+
: ''}`
|
|
1255
|
+
: ''}
|
|
1256
|
+
`
|
|
1257
|
+
: ''}
|
|
1258
|
+
## Deployment
|
|
1259
|
+
|
|
1260
|
+
### Using GitHub Actions (Automatic)
|
|
1261
|
+
|
|
1262
|
+
Push to the \`main\` or \`production\` branch:
|
|
1263
|
+
|
|
1264
|
+
\`\`\`bash
|
|
1265
|
+
git add .
|
|
1266
|
+
git commit -m "Deploy to production"
|
|
1267
|
+
git push origin main
|
|
1268
|
+
\`\`\`
|
|
1269
|
+
|
|
1270
|
+
The GitHub Actions workflow will automatically:
|
|
1271
|
+
1. Build Docker images
|
|
1272
|
+
2. Push to container registry
|
|
1273
|
+
3. Deploy to Kubernetes cluster
|
|
1274
|
+
|
|
1275
|
+
### Manual Deployment
|
|
1276
|
+
|
|
1277
|
+
#### Option 1: Using kubectl
|
|
1278
|
+
|
|
1279
|
+
\`\`\`bash
|
|
1280
|
+
# Apply all manifests
|
|
1281
|
+
${config.apps.map((app) => `kubectl apply -f k8s/${app}/`).join('\n')}
|
|
1282
|
+
${config.setupIngress ? `kubectl apply -f k8s/ingress.yaml` : ''}
|
|
1283
|
+
\`\`\`
|
|
1284
|
+
|
|
1285
|
+
#### Option 2: Using Helm
|
|
1286
|
+
|
|
1287
|
+
\`\`\`bash
|
|
1288
|
+
# Install or upgrade the release
|
|
1289
|
+
helm upgrade --install ${config.appName} ./helm/${config.appName} \\
|
|
1290
|
+
--namespace ${config.namespace} \\
|
|
1291
|
+
--create-namespace
|
|
1292
|
+
\`\`\`
|
|
1293
|
+
|
|
1294
|
+
## Monitoring
|
|
1295
|
+
|
|
1296
|
+
### Check Deployment Status
|
|
1297
|
+
|
|
1298
|
+
\`\`\`bash
|
|
1299
|
+
# Check pods
|
|
1300
|
+
kubectl get pods -n ${config.namespace}
|
|
1301
|
+
|
|
1302
|
+
# Check deployments
|
|
1303
|
+
kubectl get deployments -n ${config.namespace}
|
|
1304
|
+
|
|
1305
|
+
# Check services
|
|
1306
|
+
kubectl get services -n ${config.namespace}
|
|
1307
|
+
|
|
1308
|
+
${config.setupIngress
|
|
1309
|
+
? `# Check ingress
|
|
1310
|
+
kubectl get ingress -n ${config.namespace}
|
|
1311
|
+
`
|
|
1312
|
+
: ''}
|
|
1313
|
+
# View logs
|
|
1314
|
+
${config.apps.map((app) => `kubectl logs -f deployment/${config.appName}-${app} -n ${config.namespace}`).join('\n')}
|
|
1315
|
+
\`\`\`
|
|
1316
|
+
|
|
1317
|
+
### Scaling
|
|
1318
|
+
|
|
1319
|
+
\`\`\`bash
|
|
1320
|
+
# Scale a deployment
|
|
1321
|
+
${config.apps.map((app) => `kubectl scale deployment/${config.appName}-${app} --replicas=3 -n ${config.namespace}`).join('\n')}
|
|
1322
|
+
\`\`\`
|
|
1323
|
+
|
|
1324
|
+
## Rollback
|
|
1325
|
+
|
|
1326
|
+
\`\`\`bash
|
|
1327
|
+
# View rollout history
|
|
1328
|
+
${config.apps.map((app) => `kubectl rollout history deployment/${config.appName}-${app} -n ${config.namespace}`).join('\n')}
|
|
1329
|
+
|
|
1330
|
+
# Rollback to previous version
|
|
1331
|
+
${config.apps.map((app) => `kubectl rollout undo deployment/${config.appName}-${app} -n ${config.namespace}`).join('\n')}
|
|
1332
|
+
\`\`\`
|
|
1333
|
+
|
|
1334
|
+
## Troubleshooting
|
|
1335
|
+
|
|
1336
|
+
### View Pod Events
|
|
1337
|
+
|
|
1338
|
+
\`\`\`bash
|
|
1339
|
+
kubectl describe pod <pod-name> -n ${config.namespace}
|
|
1340
|
+
\`\`\`
|
|
1341
|
+
|
|
1342
|
+
### View Cluster Events
|
|
1343
|
+
|
|
1344
|
+
\`\`\`bash
|
|
1345
|
+
kubectl get events -n ${config.namespace} --sort-by='.lastTimestamp'
|
|
1346
|
+
\`\`\`
|
|
1347
|
+
|
|
1348
|
+
### Access Pod Shell
|
|
1349
|
+
|
|
1350
|
+
\`\`\`bash
|
|
1351
|
+
${config.apps.map((app) => `kubectl exec -it deployment/${config.appName}-${app} -n ${config.namespace} -- /bin/sh`).join('\n')}
|
|
1352
|
+
\`\`\`
|
|
1353
|
+
|
|
1354
|
+
## URLs
|
|
1355
|
+
|
|
1356
|
+
${config.domain
|
|
1357
|
+
? `- **Admin Panel:** https://${config.domain}
|
|
1358
|
+
${config.apps.includes('api')
|
|
1359
|
+
? `- **API:** https://api.${config.domain}
|
|
1360
|
+
`
|
|
1361
|
+
: ''}`
|
|
1362
|
+
: '- Configure your domain and update DNS records as described above\n'}
|
|
1363
|
+
|
|
1364
|
+
## Further Reading
|
|
1365
|
+
|
|
1366
|
+
- [Digital Ocean Kubernetes Documentation](https://docs.digitalocean.com/products/kubernetes/)
|
|
1367
|
+
- [GitHub Actions Documentation](https://docs.github.com/en/actions)
|
|
1368
|
+
- [Kubernetes Documentation](https://kubernetes.io/docs/home/)
|
|
1369
|
+
- [Helm Documentation](https://helm.sh/docs/)
|
|
1370
|
+
${config.setupSSL
|
|
1371
|
+
? `- [cert-manager Documentation](https://cert-manager.io/docs/)
|
|
1372
|
+
`
|
|
1373
|
+
: ''}
|
|
1374
|
+
`;
|
|
1375
|
+
await (0, promises_1.writeFile)(pathModule.join(path, 'DEPLOYMENT.md'), readmeContent, 'utf8');
|
|
1376
|
+
this.log(chalk.green('Created DEPLOYMENT.md'));
|
|
1377
|
+
}
|
|
1378
|
+
displayDeploymentSummary(config) {
|
|
1379
|
+
console.log(chalk.blue.bold('\nš Deployment Configuration Summary\n'));
|
|
1380
|
+
console.log(chalk.white('Provider: ') + chalk.cyan(config.provider));
|
|
1381
|
+
console.log(chalk.white('CI/CD: ') + chalk.cyan(config.cicd));
|
|
1382
|
+
console.log(chalk.white('Cluster: ') + chalk.cyan(config.clusterName));
|
|
1383
|
+
console.log(chalk.white('Namespace: ') +
|
|
1384
|
+
chalk.cyan(config.namespace) +
|
|
1385
|
+
chalk.gray(config.createNamespace ? ' (will be created)' : ' (existing)'));
|
|
1386
|
+
console.log(chalk.white('App Name: ') + chalk.cyan(config.appName));
|
|
1387
|
+
console.log(chalk.white('Registry: ') + chalk.cyan(config.containerRegistry));
|
|
1388
|
+
if (config.domain) {
|
|
1389
|
+
console.log(chalk.white('Domain: ') + chalk.cyan(config.domain));
|
|
1390
|
+
}
|
|
1391
|
+
console.log(chalk.white('Apps: ') + chalk.cyan(config.apps.join(', ')));
|
|
1392
|
+
console.log(chalk.white('Ingress: ') +
|
|
1393
|
+
chalk.cyan(config.setupIngress ? 'Yes' : 'No'));
|
|
1394
|
+
console.log(chalk.white('SSL/TLS: ') +
|
|
1395
|
+
chalk.cyan(config.setupSSL ? 'Yes' : 'No'));
|
|
1396
|
+
console.log(chalk.green.bold('\nā Generated Files:\n'));
|
|
1397
|
+
console.log(chalk.gray(' .dockerignore'));
|
|
1398
|
+
config.apps.forEach((app) => {
|
|
1399
|
+
console.log(chalk.gray(` apps/${app}/Dockerfile`));
|
|
1400
|
+
});
|
|
1401
|
+
console.log(chalk.gray(' .github/workflows/deploy.yml'));
|
|
1402
|
+
config.apps.forEach((app) => {
|
|
1403
|
+
console.log(chalk.gray(` k8s/${app}/deployment.yaml`));
|
|
1404
|
+
console.log(chalk.gray(` k8s/${app}/service.yaml`));
|
|
1405
|
+
});
|
|
1406
|
+
if (config.setupIngress) {
|
|
1407
|
+
console.log(chalk.gray(' k8s/ingress.yaml'));
|
|
1408
|
+
}
|
|
1409
|
+
if (config.setupSSL) {
|
|
1410
|
+
console.log(chalk.gray(' k8s/cert-manager-issuer.yaml'));
|
|
1411
|
+
}
|
|
1412
|
+
console.log(chalk.gray(` helm/${config.appName}/Chart.yaml`));
|
|
1413
|
+
console.log(chalk.gray(` helm/${config.appName}/values.yaml`));
|
|
1414
|
+
console.log(chalk.gray(' DEPLOYMENT.md'));
|
|
1415
|
+
console.log(chalk.blue.bold('\nš Next Steps:\n'));
|
|
1416
|
+
console.log(chalk.white('1. Review the generated files in your project'));
|
|
1417
|
+
console.log(chalk.white('2. Read DEPLOYMENT.md for setup instructions'));
|
|
1418
|
+
console.log(chalk.white('3. Configure GitHub secrets (see DEPLOYMENT.md)'));
|
|
1419
|
+
console.log(chalk.white('4. Push to main branch to trigger deployment'));
|
|
1420
|
+
console.log('');
|
|
1421
|
+
}
|
|
54
1422
|
async syncPublish(path, verbose = false) {
|
|
55
1423
|
const restoreWarnings = this.suppressWarnings();
|
|
56
1424
|
this.verbose = verbose;
|