badmfck-api-server 5.0.2 → 5.0.5
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/bluegreen.help.md +277 -0
- package/dist/apiServer/APIService.d.ts +8 -0
- package/dist/apiServer/APIService.js +24 -5
- package/dist/apiServer/LogService.d.ts +2 -0
- package/dist/apiServer/LogService.js +43 -0
- package/dist/apiServer/deployment/BlueGreenState.d.ts +31 -0
- package/dist/apiServer/deployment/BlueGreenState.js +150 -0
- package/dist/apiServer/deployment/ConfigService.d.ts +2 -0
- package/dist/apiServer/deployment/ConfigService.js +2 -0
- package/dist/apiServer/deployment/Deploy.js +27 -16
- package/dist/apiServer/deployment/Deployer.d.ts +1 -0
- package/dist/apiServer/deployment/Deployer.js +16 -0
- package/dist/apiServer/deployment/DeployerService.d.ts +34 -0
- package/dist/apiServer/deployment/DeployerService.js +78 -2
- package/dist/apiServer/deployment/Status.d.ts +2 -0
- package/dist/apiServer/deployment/Status.js +85 -0
- package/dist/apiServer/deployment/cli.js +11 -0
- package/dist/apiServer/http/Http.js +9 -1
- package/dist/apiServer/monitor/LogFiles.d.ts +34 -0
- package/dist/apiServer/monitor/LogFiles.js +109 -0
- package/dist/apiServer/monitor/Monitor.d.ts +2 -0
- package/dist/apiServer/monitor/Monitor.js +33 -0
- package/dist/apiServer/monitor/index.html +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +3 -1
- package/package.json +1 -1
|
@@ -16,23 +16,28 @@ async function Deploy(opt) {
|
|
|
16
16
|
const archiveName = __1.UID.sha256(opt.name.replaceAll(".", "_")) + ".tar.gz";
|
|
17
17
|
console.log("Changing Config to live");
|
|
18
18
|
const config = path_1.default.resolve("src", "Config.ts");
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
let replaced = 0;
|
|
22
|
-
const updated = configSrc.replace(schemeRe, (_match, prefix) => {
|
|
23
|
-
replaced++;
|
|
24
|
-
return prefix + "live";
|
|
25
|
-
});
|
|
26
|
-
if (replaced > 0) {
|
|
27
|
-
fs_1.default.writeFileSync(config, updated);
|
|
28
|
-
console.log(`Config: switched to live (${replaced} occurrence${replaced === 1 ? "" : "s"})`);
|
|
29
|
-
}
|
|
30
|
-
else if (/\bscheme\s*:\s*Scheme\s*=\s*live\b/.test(configSrc)) {
|
|
31
|
-
console.log("Config already in live mode — nothing to change");
|
|
19
|
+
if (!fs_1.default.existsSync(config)) {
|
|
20
|
+
console.log("Config: src/Config.ts not found — skipping scheme switch");
|
|
32
21
|
}
|
|
33
22
|
else {
|
|
34
|
-
|
|
35
|
-
|
|
23
|
+
const configSrc = fs_1.default.readFileSync(config).toString("utf-8");
|
|
24
|
+
const schemeRe = /(\bscheme\s*:\s*Scheme\s*=\s*)local\b/g;
|
|
25
|
+
let replaced = 0;
|
|
26
|
+
const updated = configSrc.replace(schemeRe, (_match, prefix) => {
|
|
27
|
+
replaced++;
|
|
28
|
+
return prefix + "live";
|
|
29
|
+
});
|
|
30
|
+
if (replaced > 0) {
|
|
31
|
+
fs_1.default.writeFileSync(config, updated);
|
|
32
|
+
console.log(`Config: switched to live (${replaced} occurrence${replaced === 1 ? "" : "s"})`);
|
|
33
|
+
}
|
|
34
|
+
else if (/\bscheme\s*:\s*Scheme\s*=\s*live\b/.test(configSrc)) {
|
|
35
|
+
console.log("Config already in live mode — nothing to change");
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
console.warn("Config: no 'scheme: Scheme = local|live' pattern found in " + config +
|
|
39
|
+
" — check the file was not renamed/restructured");
|
|
40
|
+
}
|
|
36
41
|
}
|
|
37
42
|
const pkgPath = path_1.default.resolve("package.json");
|
|
38
43
|
const pkg = JSON.parse(fs_1.default.readFileSync(pkgPath).toString("utf-8"));
|
|
@@ -99,6 +104,8 @@ async function Deploy(opt) {
|
|
|
99
104
|
}
|
|
100
105
|
excludes.add(trimmed);
|
|
101
106
|
};
|
|
107
|
+
addExclude("._*", "built-in");
|
|
108
|
+
addExclude(".DS_Store", "built-in");
|
|
102
109
|
const ignoreFile = path_1.default.resolve(".deployignore");
|
|
103
110
|
if (fs_1.default.existsSync(ignoreFile)) {
|
|
104
111
|
const lines = fs_1.default.readFileSync(ignoreFile, "utf-8").split(/\r?\n/);
|
|
@@ -131,7 +138,11 @@ async function Deploy(opt) {
|
|
|
131
138
|
if (excludeArgs.length > 0)
|
|
132
139
|
console.log("Excluding:", Array.from(excludes).join(", "));
|
|
133
140
|
try {
|
|
134
|
-
(0, child_process_1.execFileSync)("tar", tarArgs, {
|
|
141
|
+
(0, child_process_1.execFileSync)("tar", tarArgs, {
|
|
142
|
+
encoding: "utf-8",
|
|
143
|
+
stdio: "inherit",
|
|
144
|
+
env: { ...process.env, COPYFILE_DISABLE: "1" },
|
|
145
|
+
});
|
|
135
146
|
}
|
|
136
147
|
catch (err) {
|
|
137
148
|
console.error(`\ntar failed while creating ${archiveName}\n`);
|
|
@@ -6,4 +6,5 @@ export declare class Deployer extends BaseEndpoint {
|
|
|
6
6
|
file(req: HTTPRequestVO): Promise<TransferPacketVO<any>>;
|
|
7
7
|
switchSlot(req: HTTPRequestVO): Promise<TransferPacketVO<any>>;
|
|
8
8
|
status(req: HTTPRequestVO): Promise<TransferPacketVO<any>>;
|
|
9
|
+
slot(req: HTTPRequestVO): Promise<TransferPacketVO<any>>;
|
|
9
10
|
}
|
|
@@ -41,6 +41,19 @@ class Deployer extends BaseEndpoint_1.BaseEndpoint {
|
|
|
41
41
|
name: "",
|
|
42
42
|
$__name_optional: true
|
|
43
43
|
}
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
ignoreInterceptor: true,
|
|
47
|
+
ignoreHttpLogging: true,
|
|
48
|
+
endpoint: "slot",
|
|
49
|
+
handler: this.slot,
|
|
50
|
+
validationModel: {
|
|
51
|
+
name: "",
|
|
52
|
+
token: "",
|
|
53
|
+
slot: "",
|
|
54
|
+
$__slot_values: ["blue", "green"],
|
|
55
|
+
$__slot_optional: true
|
|
56
|
+
}
|
|
44
57
|
}
|
|
45
58
|
]);
|
|
46
59
|
}
|
|
@@ -56,5 +69,8 @@ class Deployer extends BaseEndpoint_1.BaseEndpoint {
|
|
|
56
69
|
async status(req) {
|
|
57
70
|
return await DeployerService_1.REQ_DEPLOYMENT_STATUS.request({ ...req.data, auth: req.headers.authorization });
|
|
58
71
|
}
|
|
72
|
+
async slot(req) {
|
|
73
|
+
return await DeployerService_1.REQ_DEPLOYMENT_SLOT.request({ name: req.data.name, token: req.data.token, slot: req.data.slot });
|
|
74
|
+
}
|
|
59
75
|
}
|
|
60
76
|
exports.Deployer = Deployer;
|
|
@@ -17,11 +17,15 @@ declare const _TSetup: {
|
|
|
17
17
|
readonly KEY: "";
|
|
18
18
|
};
|
|
19
19
|
readonly $__notifier_optional: true;
|
|
20
|
+
readonly status_url: "";
|
|
21
|
+
readonly $__status_url_optional: true;
|
|
20
22
|
};
|
|
21
23
|
export type TDeployerSetup = ValidationModel<typeof _TSetup>;
|
|
22
24
|
export interface IDeployerServiceOptions {
|
|
23
25
|
projects_path: string;
|
|
24
26
|
setup_path: string;
|
|
27
|
+
selfPort?: number;
|
|
28
|
+
selfBaseEndPoint?: string;
|
|
25
29
|
}
|
|
26
30
|
export declare const REQ_DEPLOYMENT_PROCEED: Req<{
|
|
27
31
|
auth: string;
|
|
@@ -41,7 +45,14 @@ export declare const REQ_DEPLOYMENT_STATUS: Req<{
|
|
|
41
45
|
auth?: string;
|
|
42
46
|
name?: string;
|
|
43
47
|
}, any>;
|
|
48
|
+
export declare const REQ_DEPLOYMENT_SLOT: Req<{
|
|
49
|
+
name: string;
|
|
50
|
+
token: string;
|
|
51
|
+
slot?: "blue" | "green";
|
|
52
|
+
}, any>;
|
|
44
53
|
export declare class DeployerService extends BaseService {
|
|
54
|
+
private static MAX_HISTORY;
|
|
55
|
+
private static _trimHistory;
|
|
45
56
|
options: IDeployerServiceOptions;
|
|
46
57
|
setup: TDeployerSetup | null;
|
|
47
58
|
notifier: IDeploymentNotifier | null;
|
|
@@ -117,6 +128,29 @@ export declare class DeployerService extends BaseService {
|
|
|
117
128
|
readyToSwitch: boolean;
|
|
118
129
|
}>;
|
|
119
130
|
}>;
|
|
131
|
+
slotState(data: {
|
|
132
|
+
name: string;
|
|
133
|
+
token: string;
|
|
134
|
+
slot?: "blue" | "green";
|
|
135
|
+
}): Promise<{
|
|
136
|
+
error: {
|
|
137
|
+
code: number;
|
|
138
|
+
message: string;
|
|
139
|
+
httpStatus: number;
|
|
140
|
+
};
|
|
141
|
+
} | {
|
|
142
|
+
data: {
|
|
143
|
+
project: string;
|
|
144
|
+
active: "blue" | "green";
|
|
145
|
+
lastDeployed: "blue" | "green";
|
|
146
|
+
readyToSwitch: boolean;
|
|
147
|
+
slot: "blue" | "green" | null;
|
|
148
|
+
isActive: boolean | null;
|
|
149
|
+
checkedAt: number;
|
|
150
|
+
};
|
|
151
|
+
}>;
|
|
152
|
+
private _statusUrl;
|
|
153
|
+
private _writeBlueGreenFiles;
|
|
120
154
|
tokensMatch(a: string | undefined, b: string | undefined): boolean;
|
|
121
155
|
private _checkUserAuth;
|
|
122
156
|
private static _extractPort;
|
|
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.DeployerService = exports.REQ_DEPLOYMENT_STATUS = exports.REQ_DEPLOYMENT_SWITCH = exports.REQ_DEPLOYMENT_PROCEED = void 0;
|
|
6
|
+
exports.DeployerService = exports.REQ_DEPLOYMENT_SLOT = exports.REQ_DEPLOYMENT_STATUS = exports.REQ_DEPLOYMENT_SWITCH = exports.REQ_DEPLOYMENT_PROCEED = void 0;
|
|
7
7
|
const badmfck_signal_1 = require("badmfck-signal");
|
|
8
8
|
const BaseService_1 = require("../BaseService");
|
|
9
9
|
const ConfigService_1 = require("./ConfigService");
|
|
@@ -16,6 +16,7 @@ const fs_1 = __importDefault(require("fs"));
|
|
|
16
16
|
const crypto_1 = __importDefault(require("crypto"));
|
|
17
17
|
const Watchdog_1 = require("./Watchdog");
|
|
18
18
|
const NginxHelper_1 = require("./NginxHelper");
|
|
19
|
+
const BlueGreenState_1 = require("./BlueGreenState");
|
|
19
20
|
const _TSetup = {
|
|
20
21
|
users: [{
|
|
21
22
|
login: "",
|
|
@@ -27,12 +28,21 @@ const _TSetup = {
|
|
|
27
28
|
URL: "",
|
|
28
29
|
KEY: "",
|
|
29
30
|
},
|
|
30
|
-
$__notifier_optional: true
|
|
31
|
+
$__notifier_optional: true,
|
|
32
|
+
status_url: "",
|
|
33
|
+
$__status_url_optional: true
|
|
31
34
|
};
|
|
32
35
|
exports.REQ_DEPLOYMENT_PROCEED = new badmfck_signal_1.Req(undefined, "REQ_DEPLOYER_SERVICE");
|
|
33
36
|
exports.REQ_DEPLOYMENT_SWITCH = new badmfck_signal_1.Req(undefined, "REQ_DEPLOYER_SWITCH");
|
|
34
37
|
exports.REQ_DEPLOYMENT_STATUS = new badmfck_signal_1.Req(undefined, "REQ_DEPLOYER_STATUS");
|
|
38
|
+
exports.REQ_DEPLOYMENT_SLOT = new badmfck_signal_1.Req(undefined, "REQ_DEPLOYER_SLOT");
|
|
35
39
|
class DeployerService extends BaseService_1.BaseService {
|
|
40
|
+
static MAX_HISTORY = 200;
|
|
41
|
+
static _trimHistory(state) {
|
|
42
|
+
const h = state.history;
|
|
43
|
+
if (Array.isArray(h) && h.length > DeployerService.MAX_HISTORY)
|
|
44
|
+
h.splice(0, h.length - DeployerService.MAX_HISTORY);
|
|
45
|
+
}
|
|
36
46
|
options;
|
|
37
47
|
setup = null;
|
|
38
48
|
notifier = null;
|
|
@@ -65,6 +75,7 @@ class DeployerService extends BaseService_1.BaseService {
|
|
|
65
75
|
exports.REQ_DEPLOYMENT_PROCEED.listener = async (data) => this.proceed(data);
|
|
66
76
|
exports.REQ_DEPLOYMENT_SWITCH.listener = async (data) => this.switchSlot(data);
|
|
67
77
|
exports.REQ_DEPLOYMENT_STATUS.listener = async (data) => this.status(data);
|
|
78
|
+
exports.REQ_DEPLOYMENT_SLOT.listener = async (data) => this.slotState(data);
|
|
68
79
|
}
|
|
69
80
|
async _loadSetup() {
|
|
70
81
|
const setupFile = path_1.default.resolve(this.options.setup_path, "setup.json");
|
|
@@ -176,12 +187,15 @@ class DeployerService extends BaseService_1.BaseService {
|
|
|
176
187
|
else {
|
|
177
188
|
projectState.lastDeployed = projectState.active === "blue" ? "green" : "blue";
|
|
178
189
|
}
|
|
190
|
+
if (!projectState.statusToken)
|
|
191
|
+
projectState.statusToken = crypto_1.default.randomBytes(32).toString("hex");
|
|
179
192
|
projectState.history.push({
|
|
180
193
|
slot: projectState.lastDeployed,
|
|
181
194
|
action: "deploy",
|
|
182
195
|
user: user.login,
|
|
183
196
|
at: Date.now()
|
|
184
197
|
});
|
|
198
|
+
DeployerService._trimHistory(projectState);
|
|
185
199
|
const subdir = projectState.lastDeployed;
|
|
186
200
|
const deployPath = path_1.default.resolve(found.destination, subdir);
|
|
187
201
|
fs_1.default.mkdirSync(deployPath, { recursive: true });
|
|
@@ -233,6 +247,9 @@ class DeployerService extends BaseService_1.BaseService {
|
|
|
233
247
|
if (found.config) {
|
|
234
248
|
await this.applyConfig(found.destination, found.config);
|
|
235
249
|
}
|
|
250
|
+
if (projectState && projectStatePath) {
|
|
251
|
+
this._writeBlueGreenFiles(data.name, projectState, path_1.default.dirname(found.destination));
|
|
252
|
+
}
|
|
236
253
|
if (found.pm2) {
|
|
237
254
|
const pm2Result = await this.runPM2(found, projectState);
|
|
238
255
|
if (__1.ErrorUtils.isError(pm2Result)) {
|
|
@@ -359,6 +376,7 @@ class DeployerService extends BaseService_1.BaseService {
|
|
|
359
376
|
user: user.login,
|
|
360
377
|
at: Date.now(),
|
|
361
378
|
});
|
|
379
|
+
DeployerService._trimHistory(projectState);
|
|
362
380
|
const tmp = projectStatePath + ".tmp";
|
|
363
381
|
fs_1.default.writeFileSync(tmp, JSON.stringify(projectState, null, 4), "utf-8");
|
|
364
382
|
fs_1.default.renameSync(tmp, projectStatePath);
|
|
@@ -418,6 +436,64 @@ class DeployerService extends BaseService_1.BaseService {
|
|
|
418
436
|
}
|
|
419
437
|
return { data: results };
|
|
420
438
|
}
|
|
439
|
+
async slotState(data) {
|
|
440
|
+
const deny = { error: { code: 53, message: "Invalid project or status token.", httpStatus: 401 } };
|
|
441
|
+
if (!data || typeof data.name !== "string" || typeof data.token !== "string" || !/^[A-Za-z0-9._-]+$/.test(data.name))
|
|
442
|
+
return deny;
|
|
443
|
+
let state = null;
|
|
444
|
+
try {
|
|
445
|
+
const statePath = path_1.default.resolve(this.options.projects_path, data.name + "_state_.json");
|
|
446
|
+
if (fs_1.default.existsSync(statePath))
|
|
447
|
+
state = JSON.parse(fs_1.default.readFileSync(statePath, "utf-8"));
|
|
448
|
+
}
|
|
449
|
+
catch {
|
|
450
|
+
state = null;
|
|
451
|
+
}
|
|
452
|
+
const tokenOk = this.tokensMatch(data.token, state?.statusToken || crypto_1.default.randomBytes(32).toString("hex"));
|
|
453
|
+
if (!state || !state.statusToken || !tokenOk)
|
|
454
|
+
return deny;
|
|
455
|
+
const slot = data.slot === "blue" || data.slot === "green" ? data.slot : null;
|
|
456
|
+
return { data: {
|
|
457
|
+
project: data.name,
|
|
458
|
+
active: state.active ?? null,
|
|
459
|
+
lastDeployed: state.lastDeployed ?? null,
|
|
460
|
+
readyToSwitch: !!(state.lastDeployed && state.lastDeployed !== state.active),
|
|
461
|
+
slot,
|
|
462
|
+
isActive: slot ? slot === state.active : null,
|
|
463
|
+
checkedAt: Date.now(),
|
|
464
|
+
} };
|
|
465
|
+
}
|
|
466
|
+
_statusUrl() {
|
|
467
|
+
const override = this.setup?.status_url;
|
|
468
|
+
if (typeof override === "string" && override.length > 0)
|
|
469
|
+
return override;
|
|
470
|
+
let base = this.options.selfBaseEndPoint ?? "/api/";
|
|
471
|
+
if (!base.startsWith("/"))
|
|
472
|
+
base = "/" + base;
|
|
473
|
+
if (!base.endsWith("/"))
|
|
474
|
+
base += "/";
|
|
475
|
+
return `http://127.0.0.1:${this.options.selfPort ?? 8080}${base}pckg/slot`;
|
|
476
|
+
}
|
|
477
|
+
_writeBlueGreenFiles(project, state, projectRoot) {
|
|
478
|
+
if (!state.statusToken)
|
|
479
|
+
return;
|
|
480
|
+
const statusUrl = this._statusUrl();
|
|
481
|
+
for (const slot of ["blue", "green"]) {
|
|
482
|
+
const dir = path_1.default.resolve(projectRoot, slot);
|
|
483
|
+
if (!fs_1.default.existsSync(dir))
|
|
484
|
+
continue;
|
|
485
|
+
const file = path_1.default.resolve(dir, BlueGreenState_1.BLUEGREEN_FILE);
|
|
486
|
+
const payload = { version: 1, project, slot, statusUrl, token: state.statusToken, writtenAt: Date.now() };
|
|
487
|
+
try {
|
|
488
|
+
const tmp = file + ".tmp";
|
|
489
|
+
fs_1.default.writeFileSync(tmp, JSON.stringify(payload, null, 2), { encoding: "utf-8", mode: 0o600 });
|
|
490
|
+
fs_1.default.renameSync(tmp, file);
|
|
491
|
+
}
|
|
492
|
+
catch (e) {
|
|
493
|
+
(0, LogService_1.logWarn)(`Cannot write ${BlueGreenState_1.BLUEGREEN_FILE} for ${project}/${slot}: ${e?.message ?? e}`);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
}
|
|
421
497
|
tokensMatch(a, b) {
|
|
422
498
|
const ha = new Uint8Array(crypto_1.default.createHash("sha256").update(String(a ?? "")).digest());
|
|
423
499
|
const hb = new Uint8Array(crypto_1.default.createHash("sha256").update(String(b ?? "")).digest());
|
|
@@ -0,0 +1,85 @@
|
|
|
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.Status = Status;
|
|
7
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
8
|
+
const __1 = require("../..");
|
|
9
|
+
const resolveConfig_1 = require("./resolveConfig");
|
|
10
|
+
async function Status(opt) {
|
|
11
|
+
opt = await (0, resolveConfig_1.resolveConfig)(opt);
|
|
12
|
+
let statusUrl;
|
|
13
|
+
if (/\/add\/?$/.test(opt.host))
|
|
14
|
+
statusUrl = opt.host.replace(/\/add\/?$/, "/status");
|
|
15
|
+
else if (opt.switch_host && /\/switch\/?$/.test(opt.switch_host))
|
|
16
|
+
statusUrl = opt.switch_host.replace(/\/switch\/?$/, "/status");
|
|
17
|
+
else
|
|
18
|
+
throw new Error(`Cannot derive status URL from host "${opt.host}" — make sure "host" ends with /add.`);
|
|
19
|
+
const authToken = crypto_1.default.createHash("sha256").update(opt.username + ":" + opt.password).digest("hex");
|
|
20
|
+
const response = await __1.Http.post(statusUrl, { name: opt.name }, {
|
|
21
|
+
headers: { authorization: `Bearer ${authToken}` },
|
|
22
|
+
timeoutMs: 15 * 1000,
|
|
23
|
+
retry: { enabled: false },
|
|
24
|
+
});
|
|
25
|
+
const useColor = process.stdout.isTTY && !process.env.NO_COLOR;
|
|
26
|
+
const c = (code, t) => useColor ? `\x1b[${code}m${t}\x1b[0m` : t;
|
|
27
|
+
const red = (t) => c("31", t), green = (t) => c("32", t), yellow = (t) => c("33", t);
|
|
28
|
+
const blue = (t) => c("34", t), dim = (t) => c("2", t), bold = (t) => c("1", t);
|
|
29
|
+
const slotColor = (s) => s === "blue" ? blue(bold("BLUE")) : s === "green" ? green(bold("GREEN")) : dim("—");
|
|
30
|
+
const line = dim("─".repeat(60));
|
|
31
|
+
console.log("");
|
|
32
|
+
if (!response.ok) {
|
|
33
|
+
console.error(red(bold("✗ STATUS FAILED — transport error")));
|
|
34
|
+
console.error(line);
|
|
35
|
+
console.error(` ${dim("host: ")} ${statusUrl}`);
|
|
36
|
+
console.error(` ${dim("status:")} ${response.status !== undefined ? `HTTP ${response.status}` : "network error (no response)"}`);
|
|
37
|
+
if (response.error !== undefined) {
|
|
38
|
+
const t = typeof response.error === "string" ? response.error : JSON.stringify(response.error);
|
|
39
|
+
console.error(` ${dim("error: ")} ${t.length > 400 ? t.slice(0, 400) + "…" : t}`);
|
|
40
|
+
}
|
|
41
|
+
console.error(line);
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
const body = response.data;
|
|
45
|
+
const appError = __1.ErrorUtils.isError(body?.error) ? body.error : __1.ErrorUtils.isError(body?.data) ? body.data : null;
|
|
46
|
+
if (appError) {
|
|
47
|
+
console.error(red(bold("✗ STATUS REJECTED BY SERVER")));
|
|
48
|
+
console.error(line);
|
|
49
|
+
console.error(` ${dim("code: ")} ${appError.code}`);
|
|
50
|
+
console.error(` ${dim("message:")} ${appError.message}`);
|
|
51
|
+
console.error(line);
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
const entry = body?.data?.[opt.name];
|
|
55
|
+
if (!entry) {
|
|
56
|
+
console.error(red(bold(`✗ Project "${opt.name}" is not configured on the deployer`)));
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
const state = entry.state;
|
|
60
|
+
console.log(`${bold("●")} ${bold(opt.name)}`);
|
|
61
|
+
console.log(line);
|
|
62
|
+
if (!entry.hasBlueGreen) {
|
|
63
|
+
console.log(` ${dim("mode: ")} single slot ${dim("(no blue-green configured)")}`);
|
|
64
|
+
}
|
|
65
|
+
else if (!state) {
|
|
66
|
+
console.log(` ${dim("mode: ")} blue-green`);
|
|
67
|
+
console.log(` ${dim("state: ")} ${yellow("never deployed")}`);
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
console.log(` ${dim("active: ")} ${slotColor(state.active)}`);
|
|
71
|
+
console.log(` ${dim("lastDeployed:")} ${slotColor(state.lastDeployed)}`);
|
|
72
|
+
if (entry.readyToSwitch)
|
|
73
|
+
console.log(` ${yellow("⚠ " + state.lastDeployed + " is deployed but NOT switched")} ${dim("→ badmfck activate")}`);
|
|
74
|
+
const last = Array.isArray(state.history) && state.history.length ? state.history[state.history.length - 1] : null;
|
|
75
|
+
if (last) {
|
|
76
|
+
const d = new Date(last.at);
|
|
77
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
78
|
+
const when = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
|
79
|
+
console.log(` ${dim("last action: ")} ${last.action} ${last.slot} ${dim("by")} ${last.user} ${dim("·")} ${when}`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
console.log(line);
|
|
83
|
+
console.log("");
|
|
84
|
+
return state?.active ?? null;
|
|
85
|
+
}
|
|
@@ -3,10 +3,12 @@
|
|
|
3
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
4
|
const Deploy_1 = require("./Deploy");
|
|
5
5
|
const Activate_1 = require("./Activate");
|
|
6
|
+
const Status_1 = require("./Status");
|
|
6
7
|
function usage() {
|
|
7
8
|
console.error("Usage:");
|
|
8
9
|
console.error(" badmfck deploy [config.json] — build + upload the current project");
|
|
9
10
|
console.error(" badmfck activate [config.json] [blue|green] — switch blue-green slot (omit slot for lastDeployed)");
|
|
11
|
+
console.error(" badmfck status [config.json] — show active / last deployed slot");
|
|
10
12
|
console.error("");
|
|
11
13
|
console.error("Config path is optional (auto-discovers deploy/ folder → deploy.json when omitted).");
|
|
12
14
|
console.error("Via npm, prefix args with -- e.g. npm run deploy -- deploy/custom.json");
|
|
@@ -51,6 +53,15 @@ async function main() {
|
|
|
51
53
|
await (0, Activate_1.Activate)(config, scheme);
|
|
52
54
|
break;
|
|
53
55
|
}
|
|
56
|
+
case "status": {
|
|
57
|
+
const { scheme, config } = parseArgs(rest);
|
|
58
|
+
if (scheme) {
|
|
59
|
+
console.error(`"status" does not take a slot argument ("${scheme}").`);
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
await (0, Status_1.Status)(config);
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
54
65
|
default:
|
|
55
66
|
if (command)
|
|
56
67
|
console.error(`Unknown command: ${command}\n`);
|
|
@@ -7,6 +7,7 @@ exports.Http = void 0;
|
|
|
7
7
|
const undici_1 = require("undici");
|
|
8
8
|
const node_diagnostics_channel_1 = __importDefault(require("node:diagnostics_channel"));
|
|
9
9
|
const MonitorService_1 = require("../MonitorService");
|
|
10
|
+
const LogService_1 = require("../LogService");
|
|
10
11
|
function sleep(ms) {
|
|
11
12
|
return new Promise((r) => setTimeout(r, ms));
|
|
12
13
|
}
|
|
@@ -138,7 +139,14 @@ class Http {
|
|
|
138
139
|
return json.length > 500 ? json.slice(0, 500) + `...(total ${json.length})` : json;
|
|
139
140
|
}
|
|
140
141
|
static _pushOutgoingLog(item) {
|
|
141
|
-
const entry = {
|
|
142
|
+
const entry = {
|
|
143
|
+
id: this._nextOutgoingLogID++,
|
|
144
|
+
...item,
|
|
145
|
+
url: typeof item.url === "string" ? LogService_1.LogService.sanitize(item.url) : item.url,
|
|
146
|
+
request: typeof item.request === "string" ? LogService_1.LogService.sanitize(item.request) : LogService_1.LogService.sanitizeDeep(item.request),
|
|
147
|
+
response: typeof item.response === "string" ? LogService_1.LogService.sanitize(item.response) : LogService_1.LogService.sanitizeDeep(item.response),
|
|
148
|
+
error: typeof item.error === "string" ? LogService_1.LogService.sanitize(item.error) : item.error,
|
|
149
|
+
};
|
|
142
150
|
this._outgoingLog.push(entry);
|
|
143
151
|
if (this._outgoingLog.length > this._outgoingLogLimit)
|
|
144
152
|
this._outgoingLog.shift();
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export declare const LOG_SOURCES: readonly ["process-out", "process-err", "nginx-access", "nginx-error"];
|
|
2
|
+
export type TLogSourceId = typeof LOG_SOURCES[number];
|
|
3
|
+
export interface ILogFilesOptions {
|
|
4
|
+
processOut?: string;
|
|
5
|
+
processErr?: string;
|
|
6
|
+
nginxAccess?: string;
|
|
7
|
+
nginxError?: string;
|
|
8
|
+
}
|
|
9
|
+
export interface ILogTailRequest {
|
|
10
|
+
lines?: number;
|
|
11
|
+
since?: number;
|
|
12
|
+
filter?: string;
|
|
13
|
+
}
|
|
14
|
+
export interface ILogTailResult {
|
|
15
|
+
source: TLogSourceId;
|
|
16
|
+
path: string;
|
|
17
|
+
size: number;
|
|
18
|
+
cursor: number;
|
|
19
|
+
rotated: boolean;
|
|
20
|
+
truncated: boolean;
|
|
21
|
+
lines: string[];
|
|
22
|
+
}
|
|
23
|
+
export declare function listLogSources(opts?: ILogFilesOptions): Promise<{
|
|
24
|
+
id: "nginx-access" | "nginx-error" | "process-err" | "process-out";
|
|
25
|
+
label: string;
|
|
26
|
+
path: string | null;
|
|
27
|
+
configured: boolean;
|
|
28
|
+
exists: boolean;
|
|
29
|
+
readable: boolean;
|
|
30
|
+
size: number | null;
|
|
31
|
+
modifiedAt: number | null;
|
|
32
|
+
reason: string | null;
|
|
33
|
+
}[]>;
|
|
34
|
+
export declare function tailLog(id: TLogSourceId, opts: ILogFilesOptions | undefined, req?: ILogTailRequest): Promise<ILogTailResult>;
|
|
@@ -0,0 +1,109 @@
|
|
|
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.LOG_SOURCES = void 0;
|
|
7
|
+
exports.listLogSources = listLogSources;
|
|
8
|
+
exports.tailLog = tailLog;
|
|
9
|
+
const promises_1 = __importDefault(require("fs/promises"));
|
|
10
|
+
const fs_1 = require("fs");
|
|
11
|
+
const LogService_1 = require("../LogService");
|
|
12
|
+
exports.LOG_SOURCES = ["process-out", "process-err", "nginx-access", "nginx-error"];
|
|
13
|
+
const LABELS = {
|
|
14
|
+
"process-out": "Process stdout (pm2 out log)",
|
|
15
|
+
"process-err": "Process stderr (pm2 error log)",
|
|
16
|
+
"nginx-access": "nginx access log",
|
|
17
|
+
"nginx-error": "nginx error log",
|
|
18
|
+
};
|
|
19
|
+
const MAX_WINDOW = 512 * 1024;
|
|
20
|
+
const MAX_LINE = 4000;
|
|
21
|
+
const MAX_LINES = 2000;
|
|
22
|
+
function resolvePaths(opts) {
|
|
23
|
+
const usable = (p) => (p && p !== "/dev/null" ? p : null);
|
|
24
|
+
return {
|
|
25
|
+
"process-out": usable(opts?.processOut) ?? usable(process.env.pm_out_log_path),
|
|
26
|
+
"process-err": usable(opts?.processErr) ?? usable(process.env.pm_err_log_path),
|
|
27
|
+
"nginx-access": usable(opts?.nginxAccess),
|
|
28
|
+
"nginx-error": usable(opts?.nginxError),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
const ANSI = /\x1b\[[0-9;]*[A-Za-z]/g;
|
|
32
|
+
function cleanLine(raw) {
|
|
33
|
+
const line = LogService_1.LogService.sanitize(raw.replace(/\r$/, "").replace(ANSI, ""));
|
|
34
|
+
return line.length > MAX_LINE ? line.slice(0, MAX_LINE) + `…(${line.length} chars)` : line;
|
|
35
|
+
}
|
|
36
|
+
async function listLogSources(opts) {
|
|
37
|
+
const paths = resolvePaths(opts);
|
|
38
|
+
return Promise.all(exports.LOG_SOURCES.map(async (id) => {
|
|
39
|
+
const path = paths[id];
|
|
40
|
+
const base = { id, label: LABELS[id], path, configured: !!path, exists: false, readable: false,
|
|
41
|
+
size: null, modifiedAt: null, reason: null };
|
|
42
|
+
if (!path)
|
|
43
|
+
return { ...base, reason: id.startsWith("process") ? "not running under pm2 and no path configured" : "path not configured (options.logFiles)" };
|
|
44
|
+
try {
|
|
45
|
+
const st = await promises_1.default.stat(path);
|
|
46
|
+
base.exists = true;
|
|
47
|
+
base.size = st.size;
|
|
48
|
+
base.modifiedAt = st.mtimeMs;
|
|
49
|
+
await promises_1.default.access(path, fs_1.constants.R_OK);
|
|
50
|
+
base.readable = true;
|
|
51
|
+
}
|
|
52
|
+
catch (e) {
|
|
53
|
+
base.reason = e?.code === "ENOENT" ? "file not found"
|
|
54
|
+
: e?.code === "EACCES" ? "permission denied for the node process user"
|
|
55
|
+
: String(e?.message ?? e);
|
|
56
|
+
}
|
|
57
|
+
return base;
|
|
58
|
+
}));
|
|
59
|
+
}
|
|
60
|
+
async function tailLog(id, opts, req = {}) {
|
|
61
|
+
const path = resolvePaths(opts)[id];
|
|
62
|
+
if (!path)
|
|
63
|
+
throw new Error(`log source "${id}" is not configured`);
|
|
64
|
+
const wantLines = Math.min(MAX_LINES, Math.max(1, Math.floor(req.lines ?? 200)));
|
|
65
|
+
const fh = await promises_1.default.open(path, "r");
|
|
66
|
+
try {
|
|
67
|
+
const { size } = await fh.stat();
|
|
68
|
+
const since = typeof req.since === "number" && Number.isFinite(req.since) ? Math.floor(req.since) : undefined;
|
|
69
|
+
let start;
|
|
70
|
+
let rotated = false, truncated = false, onBoundary = false;
|
|
71
|
+
if (since !== undefined && since >= 0 && since <= size) {
|
|
72
|
+
start = since;
|
|
73
|
+
onBoundary = true;
|
|
74
|
+
if (size - start > MAX_WINDOW) {
|
|
75
|
+
start = size - MAX_WINDOW;
|
|
76
|
+
truncated = true;
|
|
77
|
+
onBoundary = false;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
rotated = since !== undefined && since > size;
|
|
82
|
+
start = Math.max(0, size - MAX_WINDOW);
|
|
83
|
+
onBoundary = start === 0;
|
|
84
|
+
}
|
|
85
|
+
const buf = Buffer.alloc(size - start);
|
|
86
|
+
if (buf.length > 0)
|
|
87
|
+
await fh.read(buf, 0, buf.length, start);
|
|
88
|
+
const lastNl = buf.lastIndexOf(0x0a);
|
|
89
|
+
const firstNl = buf.indexOf(0x0a);
|
|
90
|
+
let begin = onBoundary ? 0 : (firstNl >= 0 ? firstNl + 1 : buf.length);
|
|
91
|
+
const end = lastNl >= 0 ? lastNl + 1 : 0;
|
|
92
|
+
if (begin > end)
|
|
93
|
+
begin = end;
|
|
94
|
+
const text = buf.subarray(begin, end).toString("utf-8");
|
|
95
|
+
let lines = text.length ? text.split("\n") : [];
|
|
96
|
+
if (lines.length && lines[lines.length - 1] === "")
|
|
97
|
+
lines.pop();
|
|
98
|
+
lines = lines.map(cleanLine);
|
|
99
|
+
const filter = typeof req.filter === "string" ? req.filter.trim().toLowerCase() : "";
|
|
100
|
+
if (filter)
|
|
101
|
+
lines = lines.filter(l => l.toLowerCase().includes(filter));
|
|
102
|
+
if (lines.length > wantLines)
|
|
103
|
+
lines = lines.slice(-wantLines);
|
|
104
|
+
return { source: id, path, size, cursor: start + end, rotated, truncated, lines };
|
|
105
|
+
}
|
|
106
|
+
finally {
|
|
107
|
+
await fh.close();
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -34,6 +34,8 @@ export declare class Monitor extends BaseEndpoint {
|
|
|
34
34
|
httpstats(req: HTTPRequestVO): Promise<TransferPacketVO>;
|
|
35
35
|
inventory(req: HTTPRequestVO): Promise<TransferPacketVO>;
|
|
36
36
|
metrics(req: HTTPRequestVO): Promise<TransferPacketVO>;
|
|
37
|
+
logfiles(req: HTTPRequestVO): Promise<TransferPacketVO>;
|
|
38
|
+
logtail(req: HTTPRequestVO): Promise<TransferPacketVO>;
|
|
37
39
|
metricsWeeks(req: HTTPRequestVO): Promise<TransferPacketVO>;
|
|
38
40
|
metricsWeek(req: HTTPRequestVO): Promise<TransferPacketVO>;
|
|
39
41
|
connections(req: HTTPRequestVO): Promise<TransferPacketVO>;
|
|
@@ -18,6 +18,7 @@ const MonitorService_1 = require("../MonitorService");
|
|
|
18
18
|
const DBService_1 = require("../DBService");
|
|
19
19
|
const SystemInfo_1 = require("./SystemInfo");
|
|
20
20
|
const ProcessMonitor_1 = require("./ProcessMonitor");
|
|
21
|
+
const LogFiles_1 = require("./LogFiles");
|
|
21
22
|
exports.S_MONITOR_REGISTRATE_ACTION = new badmfck_signal_1.Signal();
|
|
22
23
|
exports.S_MONITOR_RESTART_REQUESTED = new badmfck_signal_1.Signal();
|
|
23
24
|
const logMap = {
|
|
@@ -56,6 +57,17 @@ class Monitor extends BaseEndpoint_1.BaseEndpoint {
|
|
|
56
57
|
range: "",
|
|
57
58
|
$__range_values: ["week", "day", "hour"]
|
|
58
59
|
} },
|
|
60
|
+
{ ignoreInterceptor: true, endpoint: "logfiles", handler: this.logfiles },
|
|
61
|
+
{ ignoreInterceptor: true, endpoint: "logtail", handler: this.logtail, validationModel: {
|
|
62
|
+
source: "",
|
|
63
|
+
$__source_values: [...LogFiles_1.LOG_SOURCES],
|
|
64
|
+
lines: 0,
|
|
65
|
+
$__lines_optional: true,
|
|
66
|
+
since: 0,
|
|
67
|
+
$__since_optional: true,
|
|
68
|
+
filter: "",
|
|
69
|
+
$__filter_optional: true
|
|
70
|
+
} },
|
|
59
71
|
{ ignoreInterceptor: true, endpoint: "metricsWeeks", handler: this.metricsWeeks },
|
|
60
72
|
{ ignoreInterceptor: true, endpoint: "metricsWeek", handler: this.metricsWeek, validationModel: {
|
|
61
73
|
weekKey: ""
|
|
@@ -205,6 +217,27 @@ class Monitor extends BaseEndpoint_1.BaseEndpoint {
|
|
|
205
217
|
const result = await MonitorService_1.REQ_STAT.request(req.data);
|
|
206
218
|
return { data: result };
|
|
207
219
|
}
|
|
220
|
+
async logfiles(req) {
|
|
221
|
+
await this.checkAuthentication(req);
|
|
222
|
+
return { data: await (0, LogFiles_1.listLogSources)(this.apiServiceOptions.logFiles) };
|
|
223
|
+
}
|
|
224
|
+
async logtail(req) {
|
|
225
|
+
await this.checkAuthentication(req);
|
|
226
|
+
const num = (v) => (v === undefined || v === null || v === "" ? undefined : Number(v));
|
|
227
|
+
try {
|
|
228
|
+
return { data: await (0, LogFiles_1.tailLog)(req.data.source, this.apiServiceOptions.logFiles, {
|
|
229
|
+
lines: num(req.data.lines),
|
|
230
|
+
since: num(req.data.since),
|
|
231
|
+
filter: req.data.filter,
|
|
232
|
+
}) };
|
|
233
|
+
}
|
|
234
|
+
catch (e) {
|
|
235
|
+
const details = e?.code === "ENOENT" ? "log file not found"
|
|
236
|
+
: e?.code === "EACCES" ? "permission denied: the node process user can't read this file (nginx logs are usually root:adm 0640 — add the user to the adm group or point access_log to a readable file)"
|
|
237
|
+
: String(e?.message ?? e);
|
|
238
|
+
throw { ...DefaultErrors_1.default.BAD_REQUEST, details };
|
|
239
|
+
}
|
|
240
|
+
}
|
|
208
241
|
async metricsWeeks(req) {
|
|
209
242
|
await this.checkAuthentication(req);
|
|
210
243
|
return { data: await MonitorService_1.REQ_STAT_WEEKS.request() };
|