@openfactu/cli 0.0.7 → 0.0.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 +161 -5
- package/dist/src/commands/backup.d.ts +2 -0
- package/dist/src/commands/backup.js +424 -0
- package/dist/src/commands/deploy.js +486 -67
- package/dist/src/commands/doctor.d.ts +2 -0
- package/dist/src/commands/doctor.js +295 -0
- package/dist/src/commands/install-quick.d.ts +2 -0
- package/dist/src/commands/install-quick.js +249 -0
- package/dist/src/commands/install-script.d.ts +2 -0
- package/dist/src/commands/install-script.js +474 -0
- package/dist/src/commands/install.js +966 -72
- package/dist/src/commands/monitoring.d.ts +2 -0
- package/dist/src/commands/monitoring.js +352 -0
- package/dist/src/commands/service.d.ts +2 -0
- package/dist/src/commands/service.js +402 -0
- package/dist/src/commands/setup.js +7 -2
- package/dist/src/commands/sync-ports.d.ts +2 -0
- package/dist/src/commands/sync-ports.js +298 -0
- package/dist/src/commands/uninstall.d.ts +2 -0
- package/dist/src/commands/uninstall.js +189 -0
- package/dist/src/index.js +17 -1
- package/dist/src/utils/config.d.ts +8 -0
- package/dist/src/utils/config.js +25 -1
- package/dist/src/utils/env.d.ts +11 -0
- package/dist/src/utils/env.js +31 -0
- package/dist/src/utils/helpers.d.ts +22 -0
- package/dist/src/utils/helpers.js +244 -0
- package/dist/src/utils/monitoring.d.ts +38 -0
- package/dist/src/utils/monitoring.js +353 -0
- package/dist/src/utils/paths.d.ts +1 -0
- package/dist/src/utils/paths.js +2 -0
- package/package.json +8 -5
|
@@ -0,0 +1,244 @@
|
|
|
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.generatePassword = generatePassword;
|
|
7
|
+
exports.generateSlug = generateSlug;
|
|
8
|
+
exports.checkDiskSpace = checkDiskSpace;
|
|
9
|
+
exports.checkPortInUse = checkPortInUse;
|
|
10
|
+
exports.getRunningServices = getRunningServices;
|
|
11
|
+
exports.runPreflightChecks = runPreflightChecks;
|
|
12
|
+
exports.waitForService = waitForService;
|
|
13
|
+
exports.getDockerComposeCommand = getDockerComposeCommand;
|
|
14
|
+
exports.isLinux = isLinux;
|
|
15
|
+
exports.isSystemdAvailable = isSystemdAvailable;
|
|
16
|
+
exports.formatBytes = formatBytes;
|
|
17
|
+
exports.timestamp = timestamp;
|
|
18
|
+
exports.ensureDir = ensureDir;
|
|
19
|
+
exports.copyDirRecursive = copyDirRecursive;
|
|
20
|
+
const child_process_1 = require("child_process");
|
|
21
|
+
const fs_1 = __importDefault(require("fs"));
|
|
22
|
+
const path_1 = __importDefault(require("path"));
|
|
23
|
+
const os_1 = __importDefault(require("os"));
|
|
24
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
25
|
+
function generatePassword(length = 32) {
|
|
26
|
+
// Solo alfanumericos: los simbolos (! @ # $ % ^ & *) rompen el stack.
|
|
27
|
+
// - '$' dispara la interpolacion de variables de Docker Compose al leer el
|
|
28
|
+
// .env, vaciando la contraseña y haciendo que Postgres caiga al default.
|
|
29
|
+
// - '@ # % / : ?' rompen el parseo del connection string en DATABASE_URL.
|
|
30
|
+
// Con 62 caracteres y longitud >= 24 la entropia sigue siendo de sobra (>140 bits).
|
|
31
|
+
const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
|
32
|
+
let result = '';
|
|
33
|
+
const bytes = crypto_1.default.randomBytes(length);
|
|
34
|
+
for (let i = 0; i < length; i++) {
|
|
35
|
+
result += chars[bytes[i] % chars.length];
|
|
36
|
+
}
|
|
37
|
+
return result;
|
|
38
|
+
}
|
|
39
|
+
function generateSlug(length = 12) {
|
|
40
|
+
return crypto_1.default.randomBytes(length).toString('hex').slice(0, length);
|
|
41
|
+
}
|
|
42
|
+
function checkDiskSpace(dir) {
|
|
43
|
+
try {
|
|
44
|
+
const output = (0, child_process_1.execSync)(`df -BG "${dir}" | tail -1`, { stdio: 'pipe' }).toString();
|
|
45
|
+
const parts = output.trim().split(/\s+/);
|
|
46
|
+
return {
|
|
47
|
+
availableGB: parseInt(parts[3]) || 0,
|
|
48
|
+
totalGB: parseInt(parts[1]) || 0,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return { availableGB: 0, totalGB: 0 };
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function checkPortInUse(port) {
|
|
56
|
+
try {
|
|
57
|
+
(0, child_process_1.execSync)(`lsof -i :${port} -sTCP:LISTEN`, { stdio: 'pipe' });
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function getRunningServices() {
|
|
65
|
+
try {
|
|
66
|
+
const output = (0, child_process_1.execSync)('docker compose ps --services 2>/dev/null || docker-compose ps --services 2>/dev/null', {
|
|
67
|
+
stdio: 'pipe',
|
|
68
|
+
}).toString();
|
|
69
|
+
return output.trim().split('\n').filter(Boolean);
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return [];
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function runPreflightChecks(targetDir) {
|
|
76
|
+
const checks = [];
|
|
77
|
+
// Node.js version
|
|
78
|
+
try {
|
|
79
|
+
const version = process.version;
|
|
80
|
+
const major = parseInt(version.slice(1).split('.')[0]);
|
|
81
|
+
if (major >= 18) {
|
|
82
|
+
checks.push({ name: 'Node.js', status: 'pass', message: `${version}` });
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
checks.push({ name: 'Node.js', status: 'warn', message: `${version} (recomendado >= 18)` });
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
checks.push({ name: 'Node.js', status: 'fail', message: 'No detectado' });
|
|
90
|
+
}
|
|
91
|
+
// Git
|
|
92
|
+
try {
|
|
93
|
+
const version = (0, child_process_1.execSync)('git --version', { stdio: 'pipe' }).toString().trim();
|
|
94
|
+
checks.push({ name: 'Git', status: 'pass', message: version });
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
checks.push({ name: 'Git', status: 'fail', message: 'No instalado' });
|
|
98
|
+
}
|
|
99
|
+
// Docker
|
|
100
|
+
try {
|
|
101
|
+
const version = (0, child_process_1.execSync)('docker --version', { stdio: 'pipe' }).toString().trim();
|
|
102
|
+
checks.push({ name: 'Docker', status: 'pass', message: version });
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
checks.push({ name: 'Docker', status: 'fail', message: 'No instalado' });
|
|
106
|
+
}
|
|
107
|
+
// Docker Compose
|
|
108
|
+
try {
|
|
109
|
+
let version = '';
|
|
110
|
+
try {
|
|
111
|
+
version = (0, child_process_1.execSync)('docker compose version', { stdio: 'pipe' }).toString().trim();
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
version = (0, child_process_1.execSync)('docker-compose --version', { stdio: 'pipe' }).toString().trim();
|
|
115
|
+
}
|
|
116
|
+
checks.push({ name: 'Docker Compose', status: 'pass', message: version });
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
checks.push({ name: 'Docker Compose', status: 'fail', message: 'No instalado' });
|
|
120
|
+
}
|
|
121
|
+
// Disk space
|
|
122
|
+
if (targetDir) {
|
|
123
|
+
const dir = path_1.default.dirname(targetDir);
|
|
124
|
+
if (fs_1.default.existsSync(dir)) {
|
|
125
|
+
const disk = checkDiskSpace(dir);
|
|
126
|
+
if (disk.availableGB >= 10) {
|
|
127
|
+
checks.push({ name: 'Disco disponible', status: 'pass', message: `${disk.availableGB}GB libres` });
|
|
128
|
+
}
|
|
129
|
+
else if (disk.availableGB >= 5) {
|
|
130
|
+
checks.push({ name: 'Disco disponible', status: 'warn', message: `${disk.availableGB}GB libres (minimo 5GB)` });
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
checks.push({ name: 'Disco disponible', status: 'fail', message: `${disk.availableGB}GB libres (minimo 5GB)` });
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
// Port conflicts
|
|
138
|
+
const commonPorts = [
|
|
139
|
+
{ port: 5432, name: 'PostgreSQL' },
|
|
140
|
+
{ port: 8080, name: 'Web' },
|
|
141
|
+
{ port: 3000, name: 'API Server' },
|
|
142
|
+
{ port: 9090, name: 'Prometheus' },
|
|
143
|
+
{ port: 3001, name: 'Grafana' },
|
|
144
|
+
{ port: 5050, name: 'pgAdmin' },
|
|
145
|
+
{ port: 9000, name: 'Portainer' },
|
|
146
|
+
];
|
|
147
|
+
const conflictedPorts = commonPorts.filter(p => checkPortInUse(p.port));
|
|
148
|
+
if (conflictedPorts.length === 0) {
|
|
149
|
+
checks.push({ name: 'Puertos', status: 'pass', message: 'Sin conflictos' });
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
const portList = conflictedPorts.map(p => `${p.name}:${p.port}`).join(', ');
|
|
153
|
+
checks.push({ name: 'Puertos', status: 'warn', message: `En uso: ${portList}` });
|
|
154
|
+
}
|
|
155
|
+
// OS info
|
|
156
|
+
checks.push({ name: 'Sistema', status: 'pass', message: `${os_1.default.type()} ${os_1.default.release()} (${os_1.default.arch()})` });
|
|
157
|
+
return checks;
|
|
158
|
+
}
|
|
159
|
+
function waitForService(url, maxAttempts = 30, intervalMs = 2000) {
|
|
160
|
+
return new Promise((resolve) => {
|
|
161
|
+
let attempts = 0;
|
|
162
|
+
const http = url.startsWith('https') ? require('https') : require('http');
|
|
163
|
+
const check = () => {
|
|
164
|
+
attempts++;
|
|
165
|
+
const req = http.get(url, { timeout: 3000 }, (res) => {
|
|
166
|
+
if (res.statusCode) {
|
|
167
|
+
resolve(true);
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
retry();
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
req.on('error', () => {
|
|
174
|
+
if (attempts >= maxAttempts) {
|
|
175
|
+
resolve(false);
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
retry();
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
req.on('timeout', () => {
|
|
182
|
+
req.destroy();
|
|
183
|
+
retry();
|
|
184
|
+
});
|
|
185
|
+
};
|
|
186
|
+
const retry = () => {
|
|
187
|
+
setTimeout(check, intervalMs);
|
|
188
|
+
};
|
|
189
|
+
check();
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
function getDockerComposeCommand() {
|
|
193
|
+
try {
|
|
194
|
+
(0, child_process_1.execSync)('docker compose version', { stdio: 'pipe' });
|
|
195
|
+
return 'docker compose';
|
|
196
|
+
}
|
|
197
|
+
catch {
|
|
198
|
+
return 'docker-compose';
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
function isLinux() {
|
|
202
|
+
return os_1.default.platform() === 'linux';
|
|
203
|
+
}
|
|
204
|
+
function isSystemdAvailable() {
|
|
205
|
+
if (!isLinux())
|
|
206
|
+
return false;
|
|
207
|
+
try {
|
|
208
|
+
const pid = (0, child_process_1.execSync)('cat /run/systemd/system 2>/dev/null && echo 1 || echo 0', { stdio: 'pipe' }).toString().trim();
|
|
209
|
+
return pid === '1' || fs_1.default.existsSync('/run/systemd/system');
|
|
210
|
+
}
|
|
211
|
+
catch {
|
|
212
|
+
return false;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
function formatBytes(bytes) {
|
|
216
|
+
if (bytes === 0)
|
|
217
|
+
return '0 B';
|
|
218
|
+
const k = 1024;
|
|
219
|
+
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
220
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
221
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
222
|
+
}
|
|
223
|
+
function timestamp() {
|
|
224
|
+
return new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
|
225
|
+
}
|
|
226
|
+
function ensureDir(dir) {
|
|
227
|
+
if (!fs_1.default.existsSync(dir)) {
|
|
228
|
+
fs_1.default.mkdirSync(dir, { recursive: true });
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
function copyDirRecursive(src, dest) {
|
|
232
|
+
ensureDir(dest);
|
|
233
|
+
const entries = fs_1.default.readdirSync(src, { withFileTypes: true });
|
|
234
|
+
for (const entry of entries) {
|
|
235
|
+
const srcPath = path_1.default.join(src, entry.name);
|
|
236
|
+
const destPath = path_1.default.join(dest, entry.name);
|
|
237
|
+
if (entry.isDirectory()) {
|
|
238
|
+
copyDirRecursive(srcPath, destPath);
|
|
239
|
+
}
|
|
240
|
+
else {
|
|
241
|
+
fs_1.default.copyFileSync(srcPath, destPath);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Catálogo de servicios de monitoreo. Fuente única usada tanto por el comando
|
|
3
|
+
* `monitoring` como por `install` para construir el checkbox de selección.
|
|
4
|
+
*/
|
|
5
|
+
export interface MonitoringService {
|
|
6
|
+
value: string;
|
|
7
|
+
label: string;
|
|
8
|
+
/** Marcado por defecto en un stack básico. */
|
|
9
|
+
basic: boolean;
|
|
10
|
+
/** Pertenece al stack de analítica avanzada (logs/métricas extendidas). */
|
|
11
|
+
analytics: boolean;
|
|
12
|
+
}
|
|
13
|
+
export declare const MONITORING_CATALOG: MonitoringService[];
|
|
14
|
+
export declare const ALL_MONITORING_SERVICES: string[];
|
|
15
|
+
/** Opciones para un prompt tipo checkbox de inquirer. */
|
|
16
|
+
export declare function monitoringChoices(opts?: {
|
|
17
|
+
analytics?: boolean;
|
|
18
|
+
}): {
|
|
19
|
+
name: string;
|
|
20
|
+
value: string;
|
|
21
|
+
checked: boolean;
|
|
22
|
+
}[];
|
|
23
|
+
/** Conjunto básico (pgAdmin, Grafana, Prometheus, Portainer). */
|
|
24
|
+
export declare function basicMonitoringServices(): string[];
|
|
25
|
+
/** Conjunto completo (básico + analítica). */
|
|
26
|
+
export declare function fullMonitoringServices(): string[];
|
|
27
|
+
/** Genera el docker-compose.monitoring.yml con solo los servicios seleccionados. */
|
|
28
|
+
export declare function generateMonitoringCompose(serviceSet: Set<string>): string;
|
|
29
|
+
export declare function generatePrometheusConfig(): string;
|
|
30
|
+
export declare function generateLokiConfig(): string;
|
|
31
|
+
export declare function generatePromtailConfig(): string;
|
|
32
|
+
export declare function generateAlertmanagerConfig(): string;
|
|
33
|
+
/**
|
|
34
|
+
* Escribe los archivos de configuración necesarios para los servicios
|
|
35
|
+
* seleccionados. Cada servicio que monta un archivo de config debe generarlo
|
|
36
|
+
* aquí; si no, Docker crea un directorio en su lugar y el bind-mount falla.
|
|
37
|
+
*/
|
|
38
|
+
export declare function writeMonitoringConfigs(root: string, serviceSet: Set<string>): void;
|
|
@@ -0,0 +1,353 @@
|
|
|
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.ALL_MONITORING_SERVICES = exports.MONITORING_CATALOG = void 0;
|
|
7
|
+
exports.monitoringChoices = monitoringChoices;
|
|
8
|
+
exports.basicMonitoringServices = basicMonitoringServices;
|
|
9
|
+
exports.fullMonitoringServices = fullMonitoringServices;
|
|
10
|
+
exports.generateMonitoringCompose = generateMonitoringCompose;
|
|
11
|
+
exports.generatePrometheusConfig = generatePrometheusConfig;
|
|
12
|
+
exports.generateLokiConfig = generateLokiConfig;
|
|
13
|
+
exports.generatePromtailConfig = generatePromtailConfig;
|
|
14
|
+
exports.generateAlertmanagerConfig = generateAlertmanagerConfig;
|
|
15
|
+
exports.writeMonitoringConfigs = writeMonitoringConfigs;
|
|
16
|
+
const fs_1 = __importDefault(require("fs"));
|
|
17
|
+
const path_1 = __importDefault(require("path"));
|
|
18
|
+
exports.MONITORING_CATALOG = [
|
|
19
|
+
{ value: 'pgadmin', label: 'pgAdmin (gestion BD)', basic: true, analytics: false },
|
|
20
|
+
{ value: 'grafana', label: 'Grafana (dashboards)', basic: true, analytics: false },
|
|
21
|
+
{ value: 'prometheus', label: 'Prometheus (metricas)', basic: true, analytics: false },
|
|
22
|
+
{ value: 'loki', label: 'Loki (agregacion de logs)', basic: false, analytics: true },
|
|
23
|
+
{ value: 'promtail', label: 'Promtail (envio de logs a Loki)', basic: false, analytics: true },
|
|
24
|
+
{ value: 'cadvisor', label: 'cAdvisor (metricas contenedores)', basic: false, analytics: true },
|
|
25
|
+
{ value: 'node-exporter', label: 'Node Exporter (metricas host)', basic: false, analytics: true },
|
|
26
|
+
{ value: 'portainer', label: 'Portainer (gestion Docker)', basic: true, analytics: false },
|
|
27
|
+
{ value: 'alertmanager', label: 'Alertmanager (alertas)', basic: false, analytics: false },
|
|
28
|
+
];
|
|
29
|
+
exports.ALL_MONITORING_SERVICES = exports.MONITORING_CATALOG.map((s) => s.value);
|
|
30
|
+
/** Opciones para un prompt tipo checkbox de inquirer. */
|
|
31
|
+
function monitoringChoices(opts = {}) {
|
|
32
|
+
return exports.MONITORING_CATALOG.map((s) => ({
|
|
33
|
+
name: s.label,
|
|
34
|
+
value: s.value,
|
|
35
|
+
checked: s.basic || (opts.analytics ? s.analytics : false),
|
|
36
|
+
}));
|
|
37
|
+
}
|
|
38
|
+
/** Conjunto básico (pgAdmin, Grafana, Prometheus, Portainer). */
|
|
39
|
+
function basicMonitoringServices() {
|
|
40
|
+
return exports.MONITORING_CATALOG.filter((s) => s.basic).map((s) => s.value);
|
|
41
|
+
}
|
|
42
|
+
/** Conjunto completo (básico + analítica). */
|
|
43
|
+
function fullMonitoringServices() {
|
|
44
|
+
return exports.MONITORING_CATALOG.filter((s) => s.basic || s.analytics).map((s) => s.value);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Renderiza un bloque `depends_on:` con solo las dependencias que están en el
|
|
48
|
+
* conjunto seleccionado (evita "depends on undefined service" cuando se elige
|
|
49
|
+
* un subconjunto). Devuelve '' si no queda ninguna dependencia presente.
|
|
50
|
+
*/
|
|
51
|
+
function dependsOn(serviceSet, deps) {
|
|
52
|
+
const present = deps.filter((d) => serviceSet.has(d));
|
|
53
|
+
if (present.length === 0)
|
|
54
|
+
return '';
|
|
55
|
+
return ` depends_on:\n` + present.map((d) => ` - ${d}`).join('\n') + '\n';
|
|
56
|
+
}
|
|
57
|
+
/** Genera el docker-compose.monitoring.yml con solo los servicios seleccionados. */
|
|
58
|
+
function generateMonitoringCompose(serviceSet) {
|
|
59
|
+
let compose = `# OpenFactu Monitoring Stack
|
|
60
|
+
# Generated by @openfactu/cli
|
|
61
|
+
# Services: ${Array.from(serviceSet).join(', ')}
|
|
62
|
+
|
|
63
|
+
services:
|
|
64
|
+
`;
|
|
65
|
+
if (serviceSet.has('pgadmin')) {
|
|
66
|
+
compose += `
|
|
67
|
+
pgadmin:
|
|
68
|
+
image: dpage/pgadmin4:latest
|
|
69
|
+
container_name: openfactu-pgadmin
|
|
70
|
+
environment:
|
|
71
|
+
PGADMIN_DEFAULT_EMAIL: \${PGADMIN_EMAIL:-admin@openfactu.local}
|
|
72
|
+
PGADMIN_DEFAULT_PASSWORD: \${PGADMIN_PASSWORD:-admin}
|
|
73
|
+
PGADMIN_CONFIG_SERVER_MODE: 'False'
|
|
74
|
+
ports:
|
|
75
|
+
- "\${PGADMIN_PORT:-5050}:80"
|
|
76
|
+
volumes:
|
|
77
|
+
- ./storage/pgadmin_data:/var/lib/pgadmin
|
|
78
|
+
restart: unless-stopped
|
|
79
|
+
networks:
|
|
80
|
+
- openfactu_net
|
|
81
|
+
`;
|
|
82
|
+
}
|
|
83
|
+
if (serviceSet.has('prometheus')) {
|
|
84
|
+
compose += `
|
|
85
|
+
prometheus:
|
|
86
|
+
image: prom/prometheus:latest
|
|
87
|
+
container_name: openfactu-prometheus
|
|
88
|
+
command:
|
|
89
|
+
- '--config.file=/etc/prometheus/prometheus.yml'
|
|
90
|
+
- '--storage.tsdb.path=/prometheus'
|
|
91
|
+
- '--storage.tsdb.retention.time=15d'
|
|
92
|
+
- '--web.enable-lifecycle'
|
|
93
|
+
- '--web.enable-admin-api'
|
|
94
|
+
ports:
|
|
95
|
+
- "\${PROMETHEUS_PORT:-9090}:9090"
|
|
96
|
+
volumes:
|
|
97
|
+
- ./monitoring/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro
|
|
98
|
+
- ./storage/prometheus_data:/prometheus
|
|
99
|
+
restart: unless-stopped
|
|
100
|
+
networks:
|
|
101
|
+
- openfactu_net
|
|
102
|
+
`;
|
|
103
|
+
}
|
|
104
|
+
if (serviceSet.has('grafana')) {
|
|
105
|
+
compose += `
|
|
106
|
+
grafana:
|
|
107
|
+
image: grafana/grafana:latest
|
|
108
|
+
container_name: openfactu-grafana
|
|
109
|
+
environment:
|
|
110
|
+
- GF_SECURITY_ADMIN_USER=\${GRAFANA_USER:-admin}
|
|
111
|
+
- GF_SECURITY_ADMIN_PASSWORD=\${GRAFANA_PASSWORD:-admin}
|
|
112
|
+
- GF_USERS_ALLOW_SIGN_UP=false
|
|
113
|
+
- GF_INSTALL_PLUGINS=grafana-piechart-panel
|
|
114
|
+
ports:
|
|
115
|
+
- "\${GRAFANA_PORT:-3001}:3000"
|
|
116
|
+
volumes:
|
|
117
|
+
- ./storage/grafana_data:/var/lib/grafana
|
|
118
|
+
- ./monitoring/grafana/provisioning:/etc/grafana/provisioning:ro
|
|
119
|
+
${dependsOn(serviceSet, ['prometheus', 'loki'])} restart: unless-stopped
|
|
120
|
+
networks:
|
|
121
|
+
- openfactu_net
|
|
122
|
+
`;
|
|
123
|
+
}
|
|
124
|
+
if (serviceSet.has('loki')) {
|
|
125
|
+
compose += `
|
|
126
|
+
loki:
|
|
127
|
+
image: grafana/loki:latest
|
|
128
|
+
container_name: openfactu-loki
|
|
129
|
+
command: -config.file=/etc/loki/local-config.yaml
|
|
130
|
+
ports:
|
|
131
|
+
- "\${LOKI_PORT:-3100}:3100"
|
|
132
|
+
volumes:
|
|
133
|
+
- ./storage/loki_data:/loki
|
|
134
|
+
- ./monitoring/loki/loki-config.yaml:/etc/loki/local-config.yaml:ro
|
|
135
|
+
restart: unless-stopped
|
|
136
|
+
networks:
|
|
137
|
+
- openfactu_net
|
|
138
|
+
`;
|
|
139
|
+
}
|
|
140
|
+
if (serviceSet.has('promtail')) {
|
|
141
|
+
compose += `
|
|
142
|
+
promtail:
|
|
143
|
+
image: grafana/promtail:latest
|
|
144
|
+
container_name: openfactu-promtail
|
|
145
|
+
command: -config.file=/etc/promtail/config.yml
|
|
146
|
+
volumes:
|
|
147
|
+
- ./monitoring/promtail/promtail-config.yaml:/etc/promtail/config.yml:ro
|
|
148
|
+
- /var/log:/var/log
|
|
149
|
+
- ./storage:/app/storage:ro
|
|
150
|
+
${dependsOn(serviceSet, ['loki'])} restart: unless-stopped
|
|
151
|
+
networks:
|
|
152
|
+
- openfactu_net
|
|
153
|
+
`;
|
|
154
|
+
}
|
|
155
|
+
if (serviceSet.has('cadvisor')) {
|
|
156
|
+
compose += `
|
|
157
|
+
cadvisor:
|
|
158
|
+
image: gcr.io/cadvisor/cadvisor:latest
|
|
159
|
+
container_name: openfactu-cadvisor
|
|
160
|
+
ports:
|
|
161
|
+
- "\${CADVISOR_PORT:-8081}:8080"
|
|
162
|
+
volumes:
|
|
163
|
+
- /:/rootfs:ro
|
|
164
|
+
- /var/run:/var/run:ro
|
|
165
|
+
- /sys:/sys:ro
|
|
166
|
+
- /var/lib/docker/:/var/lib/docker:ro
|
|
167
|
+
- /dev/disk/:/dev/disk:ro
|
|
168
|
+
devices:
|
|
169
|
+
- /dev/kmsg
|
|
170
|
+
restart: unless-stopped
|
|
171
|
+
networks:
|
|
172
|
+
- openfactu_net
|
|
173
|
+
`;
|
|
174
|
+
}
|
|
175
|
+
if (serviceSet.has('node-exporter')) {
|
|
176
|
+
compose += `
|
|
177
|
+
node-exporter:
|
|
178
|
+
image: prom/node-exporter:latest
|
|
179
|
+
container_name: openfactu-node-exporter
|
|
180
|
+
command:
|
|
181
|
+
- '--path.rootfs=/host'
|
|
182
|
+
ports:
|
|
183
|
+
- "\${NODE_EXPORTER_PORT:-9100}:9100"
|
|
184
|
+
volumes:
|
|
185
|
+
- /:/host:ro,rslave
|
|
186
|
+
restart: unless-stopped
|
|
187
|
+
networks:
|
|
188
|
+
- openfactu_net
|
|
189
|
+
`;
|
|
190
|
+
}
|
|
191
|
+
if (serviceSet.has('portainer')) {
|
|
192
|
+
compose += `
|
|
193
|
+
portainer:
|
|
194
|
+
image: portainer/portainer-ce:latest
|
|
195
|
+
container_name: openfactu-portainer
|
|
196
|
+
command: -H unix:///var/run/docker.sock
|
|
197
|
+
ports:
|
|
198
|
+
- "\${PORTAINER_PORT:-9000}:9000"
|
|
199
|
+
volumes:
|
|
200
|
+
- /var/run/docker.sock:/var/run/docker.sock:ro
|
|
201
|
+
- ./storage/portainer_data:/data
|
|
202
|
+
restart: unless-stopped
|
|
203
|
+
networks:
|
|
204
|
+
- openfactu_net
|
|
205
|
+
`;
|
|
206
|
+
}
|
|
207
|
+
if (serviceSet.has('alertmanager')) {
|
|
208
|
+
compose += `
|
|
209
|
+
alertmanager:
|
|
210
|
+
image: prom/alertmanager:latest
|
|
211
|
+
container_name: openfactu-alertmanager
|
|
212
|
+
command:
|
|
213
|
+
- '--config.file=/etc/alertmanager/alertmanager.yml'
|
|
214
|
+
- '--storage.path=/alertmanager'
|
|
215
|
+
ports:
|
|
216
|
+
- "\${ALERTMANAGER_PORT:-9093}:9093"
|
|
217
|
+
volumes:
|
|
218
|
+
- ./monitoring/alertmanager/alertmanager.yml:/etc/alertmanager/alertmanager.yml:ro
|
|
219
|
+
- ./storage/alertmanager_data:/alertmanager
|
|
220
|
+
restart: unless-stopped
|
|
221
|
+
networks:
|
|
222
|
+
- openfactu_net
|
|
223
|
+
`;
|
|
224
|
+
}
|
|
225
|
+
compose += `
|
|
226
|
+
networks:
|
|
227
|
+
openfactu_net:
|
|
228
|
+
name: openfactu_net
|
|
229
|
+
driver: bridge
|
|
230
|
+
`;
|
|
231
|
+
return compose;
|
|
232
|
+
}
|
|
233
|
+
function generatePrometheusConfig() {
|
|
234
|
+
return `global:
|
|
235
|
+
scrape_interval: 15s
|
|
236
|
+
evaluation_interval: 15s
|
|
237
|
+
|
|
238
|
+
scrape_configs:
|
|
239
|
+
- job_name: 'prometheus'
|
|
240
|
+
static_configs:
|
|
241
|
+
- targets: ['localhost:9090']
|
|
242
|
+
|
|
243
|
+
- job_name: 'node-exporter'
|
|
244
|
+
static_configs:
|
|
245
|
+
- targets: ['node-exporter:9100']
|
|
246
|
+
|
|
247
|
+
- job_name: 'cadvisor'
|
|
248
|
+
static_configs:
|
|
249
|
+
- targets: ['cadvisor:8080']
|
|
250
|
+
|
|
251
|
+
- job_name: 'openfactu-server'
|
|
252
|
+
metrics_path: '/api/metrics'
|
|
253
|
+
static_configs:
|
|
254
|
+
- targets: ['server:3000']
|
|
255
|
+
`;
|
|
256
|
+
}
|
|
257
|
+
function generateLokiConfig() {
|
|
258
|
+
return `auth_enabled: false
|
|
259
|
+
|
|
260
|
+
server:
|
|
261
|
+
http_listen_port: 3100
|
|
262
|
+
|
|
263
|
+
common:
|
|
264
|
+
path_prefix: /loki
|
|
265
|
+
storage:
|
|
266
|
+
filesystem:
|
|
267
|
+
chunks_directory: /loki/chunks
|
|
268
|
+
rules_directory: /loki/rules
|
|
269
|
+
replication_factor: 1
|
|
270
|
+
ring:
|
|
271
|
+
kvstore:
|
|
272
|
+
store: inmemory
|
|
273
|
+
|
|
274
|
+
schema_config:
|
|
275
|
+
configs:
|
|
276
|
+
- from: 2020-10-24
|
|
277
|
+
store: boltdb-shipper
|
|
278
|
+
object_store: filesystem
|
|
279
|
+
schema: v11
|
|
280
|
+
index:
|
|
281
|
+
prefix: index_
|
|
282
|
+
period: 24h
|
|
283
|
+
|
|
284
|
+
limits_config:
|
|
285
|
+
reject_old_samples: true
|
|
286
|
+
reject_old_samples_max_age: 168h
|
|
287
|
+
`;
|
|
288
|
+
}
|
|
289
|
+
function generatePromtailConfig() {
|
|
290
|
+
return `server:
|
|
291
|
+
http_listen_port: 9080
|
|
292
|
+
grpc_listen_port: 0
|
|
293
|
+
|
|
294
|
+
positions:
|
|
295
|
+
filename: /tmp/positions.yaml
|
|
296
|
+
|
|
297
|
+
clients:
|
|
298
|
+
- url: http://loki:3100/loki/api/v1/push
|
|
299
|
+
|
|
300
|
+
scrape_configs:
|
|
301
|
+
- job_name: openfactu-logs
|
|
302
|
+
static_configs:
|
|
303
|
+
- targets:
|
|
304
|
+
- localhost
|
|
305
|
+
labels:
|
|
306
|
+
job: openfactu
|
|
307
|
+
__path__: /app/storage/**/*.log
|
|
308
|
+
`;
|
|
309
|
+
}
|
|
310
|
+
function generateAlertmanagerConfig() {
|
|
311
|
+
return `route:
|
|
312
|
+
receiver: 'default'
|
|
313
|
+
group_by: ['alertname']
|
|
314
|
+
group_wait: 30s
|
|
315
|
+
group_interval: 5m
|
|
316
|
+
repeat_interval: 3h
|
|
317
|
+
|
|
318
|
+
receivers:
|
|
319
|
+
- name: 'default'
|
|
320
|
+
`;
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Escribe los archivos de configuración necesarios para los servicios
|
|
324
|
+
* seleccionados. Cada servicio que monta un archivo de config debe generarlo
|
|
325
|
+
* aquí; si no, Docker crea un directorio en su lugar y el bind-mount falla.
|
|
326
|
+
*/
|
|
327
|
+
function writeMonitoringConfigs(root, serviceSet) {
|
|
328
|
+
if (serviceSet.has('prometheus')) {
|
|
329
|
+
const dir = path_1.default.join(root, 'monitoring/prometheus');
|
|
330
|
+
fs_1.default.mkdirSync(dir, { recursive: true });
|
|
331
|
+
fs_1.default.writeFileSync(path_1.default.join(dir, 'prometheus.yml'), generatePrometheusConfig());
|
|
332
|
+
}
|
|
333
|
+
if (serviceSet.has('loki')) {
|
|
334
|
+
const dir = path_1.default.join(root, 'monitoring/loki');
|
|
335
|
+
fs_1.default.mkdirSync(dir, { recursive: true });
|
|
336
|
+
fs_1.default.writeFileSync(path_1.default.join(dir, 'loki-config.yaml'), generateLokiConfig());
|
|
337
|
+
}
|
|
338
|
+
if (serviceSet.has('promtail')) {
|
|
339
|
+
const dir = path_1.default.join(root, 'monitoring/promtail');
|
|
340
|
+
fs_1.default.mkdirSync(dir, { recursive: true });
|
|
341
|
+
fs_1.default.writeFileSync(path_1.default.join(dir, 'promtail-config.yaml'), generatePromtailConfig());
|
|
342
|
+
}
|
|
343
|
+
if (serviceSet.has('alertmanager')) {
|
|
344
|
+
const dir = path_1.default.join(root, 'monitoring/alertmanager');
|
|
345
|
+
fs_1.default.mkdirSync(dir, { recursive: true });
|
|
346
|
+
fs_1.default.writeFileSync(path_1.default.join(dir, 'alertmanager.yml'), generateAlertmanagerConfig());
|
|
347
|
+
}
|
|
348
|
+
if (serviceSet.has('grafana')) {
|
|
349
|
+
// El servicio monta ./monitoring/grafana/provisioning (directorio :ro).
|
|
350
|
+
// Lo creamos vacío para que el mount tenga una fuente real.
|
|
351
|
+
fs_1.default.mkdirSync(path_1.default.join(root, 'monitoring/grafana/provisioning'), { recursive: true });
|
|
352
|
+
}
|
|
353
|
+
}
|
package/dist/src/utils/paths.js
CHANGED
|
@@ -10,6 +10,7 @@ exports.getServerSrcDir = getServerSrcDir;
|
|
|
10
10
|
exports.getMigrationsDir = getMigrationsDir;
|
|
11
11
|
exports.getPluginsDir = getPluginsDir;
|
|
12
12
|
exports.getEnvPath = getEnvPath;
|
|
13
|
+
exports.getMonitoringComposePath = getMonitoringComposePath;
|
|
13
14
|
const path_1 = __importDefault(require("path"));
|
|
14
15
|
const fs_1 = __importDefault(require("fs"));
|
|
15
16
|
let _projectRoot = null;
|
|
@@ -82,3 +83,4 @@ function getServerSrcDir() { return path_1.default.join(getServerDir(), 'src');
|
|
|
82
83
|
function getMigrationsDir() { return path_1.default.join(getServerSrcDir(), 'core/tenant/migrations'); }
|
|
83
84
|
function getPluginsDir() { return path_1.default.join(getProjectRoot(), 'plugins'); }
|
|
84
85
|
function getEnvPath() { return path_1.default.join(getProjectRoot(), '.env'); }
|
|
86
|
+
function getMonitoringComposePath() { return path_1.default.join(getProjectRoot(), 'docker-compose.monitoring.yml'); }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openfactu/cli",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.8",
|
|
4
4
|
"description": "CLI para gestionar OpenFactu: migraciones, tenants, plugins y setup",
|
|
5
5
|
"main": "./dist/src/index.js",
|
|
6
6
|
"types": "./dist/src/index.d.ts",
|
|
@@ -16,10 +16,12 @@
|
|
|
16
16
|
},
|
|
17
17
|
"scripts": {
|
|
18
18
|
"build": "tsc",
|
|
19
|
-
"dev": "ts-node bin/openfactu.ts"
|
|
19
|
+
"dev": "ts-node bin/openfactu.ts",
|
|
20
|
+
"test": "vitest run",
|
|
21
|
+
"test:watch": "vitest",
|
|
22
|
+
"test:coverage": "vitest run --coverage"
|
|
20
23
|
},
|
|
21
24
|
"dependencies": {
|
|
22
|
-
"bcrypt": "^5.1.1",
|
|
23
25
|
"chalk": "^4.1.2",
|
|
24
26
|
"cli-table3": "^0.6.5",
|
|
25
27
|
"commander": "^12.0.0",
|
|
@@ -31,10 +33,11 @@
|
|
|
31
33
|
"pg": "^8.13.0"
|
|
32
34
|
},
|
|
33
35
|
"devDependencies": {
|
|
34
|
-
"@types/bcrypt": "^5.0.0",
|
|
35
36
|
"@types/inquirer": "^8.2.0",
|
|
36
37
|
"@types/pg": "^8.11.0",
|
|
38
|
+
"@vitest/coverage-v8": "^4.1.6",
|
|
37
39
|
"ts-node": "^10.9.0",
|
|
38
|
-
"typescript": "^5.3.3"
|
|
40
|
+
"typescript": "^5.3.3",
|
|
41
|
+
"vitest": "^4.1.6"
|
|
39
42
|
}
|
|
40
43
|
}
|