badmfck-api-server 5.0.3 → 5.0.6
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 +23 -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 +70 -25
- 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/deployment/resolveConfig.d.ts +4 -1
- package/dist/apiServer/deployment/resolveConfig.js +14 -2
- 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/index.d.ts +2 -0
- package/dist/index.js +3 -1
- package/package.json +1 -1
|
@@ -9,6 +9,7 @@ const path_1 = __importDefault(require("path"));
|
|
|
9
9
|
const fs_1 = __importDefault(require("fs"));
|
|
10
10
|
const __1 = require("../..");
|
|
11
11
|
const crypto_1 = __importDefault(require("crypto"));
|
|
12
|
+
const os_1 = __importDefault(require("os"));
|
|
12
13
|
const undici_1 = require("undici");
|
|
13
14
|
const resolveConfig_1 = require("./resolveConfig");
|
|
14
15
|
async function Deploy(opt) {
|
|
@@ -16,23 +17,28 @@ async function Deploy(opt) {
|
|
|
16
17
|
const archiveName = __1.UID.sha256(opt.name.replaceAll(".", "_")) + ".tar.gz";
|
|
17
18
|
console.log("Changing Config to live");
|
|
18
19
|
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");
|
|
20
|
+
if (!fs_1.default.existsSync(config)) {
|
|
21
|
+
console.log("Config: src/Config.ts not found — skipping scheme switch");
|
|
32
22
|
}
|
|
33
23
|
else {
|
|
34
|
-
|
|
35
|
-
|
|
24
|
+
const configSrc = fs_1.default.readFileSync(config).toString("utf-8");
|
|
25
|
+
const schemeRe = /(\bscheme\s*:\s*Scheme\s*=\s*)local\b/g;
|
|
26
|
+
let replaced = 0;
|
|
27
|
+
const updated = configSrc.replace(schemeRe, (_match, prefix) => {
|
|
28
|
+
replaced++;
|
|
29
|
+
return prefix + "live";
|
|
30
|
+
});
|
|
31
|
+
if (replaced > 0) {
|
|
32
|
+
fs_1.default.writeFileSync(config, updated);
|
|
33
|
+
console.log(`Config: switched to live (${replaced} occurrence${replaced === 1 ? "" : "s"})`);
|
|
34
|
+
}
|
|
35
|
+
else if (/\bscheme\s*:\s*Scheme\s*=\s*live\b/.test(configSrc)) {
|
|
36
|
+
console.log("Config already in live mode — nothing to change");
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
console.warn("Config: no 'scheme: Scheme = local|live' pattern found in " + config +
|
|
40
|
+
" — check the file was not renamed/restructured");
|
|
41
|
+
}
|
|
36
42
|
}
|
|
37
43
|
const pkgPath = path_1.default.resolve("package.json");
|
|
38
44
|
const pkg = JSON.parse(fs_1.default.readFileSync(pkgPath).toString("utf-8"));
|
|
@@ -69,21 +75,33 @@ async function Deploy(opt) {
|
|
|
69
75
|
spaMode = true;
|
|
70
76
|
console.log("No ./bin found — using entire ./dist contents as distribution");
|
|
71
77
|
}
|
|
78
|
+
const staged = [];
|
|
72
79
|
if (Array.isArray(opt.includes)) {
|
|
73
|
-
for (const
|
|
74
|
-
|
|
80
|
+
for (const raw of opt.includes) {
|
|
81
|
+
const inc = typeof raw === "string" ? { src: raw, dest: raw } : raw;
|
|
82
|
+
if (!inc || typeof inc.src !== "string" || inc.src.length === 0)
|
|
75
83
|
continue;
|
|
76
|
-
|
|
77
|
-
|
|
84
|
+
const src = inc.src;
|
|
85
|
+
const dest = (inc.dest && inc.dest.length > 0 ? inc.dest : src).replace(/^\.\//, "");
|
|
86
|
+
if (src.startsWith("-") || dest.startsWith("-"))
|
|
87
|
+
throw new Error(`Include path must not start with '-': ${src} -> ${dest}`);
|
|
88
|
+
if (!fs_1.default.existsSync(src))
|
|
89
|
+
throw new Error(`Include source does not exist: ${src}`);
|
|
90
|
+
if (path_1.default.isAbsolute(dest) || dest.split(/[\\/]/).includes(".."))
|
|
91
|
+
throw new Error(`Include 'dest' must be a relative path inside the slot, without '..': ${dest}`);
|
|
92
|
+
if (dest === src.replace(/^\.\//, "")) {
|
|
93
|
+
items.push(src);
|
|
78
94
|
continue;
|
|
79
95
|
}
|
|
80
|
-
|
|
81
|
-
console.warn(`Include path does not exist (tar will error): ${inc}`);
|
|
82
|
-
}
|
|
83
|
-
items.push(inc);
|
|
96
|
+
staged.push({ src, dest });
|
|
84
97
|
}
|
|
85
98
|
}
|
|
86
|
-
|
|
99
|
+
for (const s of staged) {
|
|
100
|
+
const clash = items.some(i => i.replace(/^\.\//, "") === s.dest);
|
|
101
|
+
if (clash)
|
|
102
|
+
console.warn(`Include dest "${s.dest}" collides with an already-archived path — the later entry wins on extraction`);
|
|
103
|
+
}
|
|
104
|
+
if (items.length === 0 && !spaMode && staged.length === 0) {
|
|
87
105
|
throw new Error("Nothing to archive (no ./bin, no ./dist/index.html, no valid includes)");
|
|
88
106
|
}
|
|
89
107
|
const excludes = new Set();
|
|
@@ -99,6 +117,8 @@ async function Deploy(opt) {
|
|
|
99
117
|
}
|
|
100
118
|
excludes.add(trimmed);
|
|
101
119
|
};
|
|
120
|
+
addExclude("._*", "built-in");
|
|
121
|
+
addExclude(".DS_Store", "built-in");
|
|
102
122
|
const ignoreFile = path_1.default.resolve(".deployignore");
|
|
103
123
|
if (fs_1.default.existsSync(ignoreFile)) {
|
|
104
124
|
const lines = fs_1.default.readFileSync(ignoreFile, "utf-8").split(/\r?\n/);
|
|
@@ -120,6 +140,17 @@ async function Deploy(opt) {
|
|
|
120
140
|
else {
|
|
121
141
|
tarArgs.push(...items);
|
|
122
142
|
}
|
|
143
|
+
let stageDir = null;
|
|
144
|
+
if (staged.length > 0) {
|
|
145
|
+
stageDir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), "badmfck-deploy-"));
|
|
146
|
+
for (const { src, dest } of staged) {
|
|
147
|
+
const target = path_1.default.join(stageDir, dest);
|
|
148
|
+
fs_1.default.mkdirSync(path_1.default.dirname(target), { recursive: true });
|
|
149
|
+
fs_1.default.cpSync(path_1.default.resolve(src), target, { recursive: true });
|
|
150
|
+
console.log(`Include: ${src} -> ${dest}`);
|
|
151
|
+
}
|
|
152
|
+
tarArgs.push("-C", stageDir, ".");
|
|
153
|
+
}
|
|
123
154
|
if (spaMode) {
|
|
124
155
|
console.log("Archiving: contents of ./dist (recursively, at archive root)");
|
|
125
156
|
if (items.length > 0)
|
|
@@ -131,7 +162,11 @@ async function Deploy(opt) {
|
|
|
131
162
|
if (excludeArgs.length > 0)
|
|
132
163
|
console.log("Excluding:", Array.from(excludes).join(", "));
|
|
133
164
|
try {
|
|
134
|
-
(0, child_process_1.execFileSync)("tar", tarArgs, {
|
|
165
|
+
(0, child_process_1.execFileSync)("tar", tarArgs, {
|
|
166
|
+
encoding: "utf-8",
|
|
167
|
+
stdio: "inherit",
|
|
168
|
+
env: { ...process.env, COPYFILE_DISABLE: "1" },
|
|
169
|
+
});
|
|
135
170
|
}
|
|
136
171
|
catch (err) {
|
|
137
172
|
console.error(`\ntar failed while creating ${archiveName}\n`);
|
|
@@ -143,6 +178,16 @@ async function Deploy(opt) {
|
|
|
143
178
|
console.error(errOut);
|
|
144
179
|
process.exit(typeof err.status === "number" ? err.status : 1);
|
|
145
180
|
}
|
|
181
|
+
finally {
|
|
182
|
+
if (stageDir) {
|
|
183
|
+
try {
|
|
184
|
+
fs_1.default.rmSync(stageDir, { recursive: true, force: true });
|
|
185
|
+
}
|
|
186
|
+
catch (e) {
|
|
187
|
+
console.warn(`Could not remove staging dir ${stageDir}: ${e?.message ?? e}`);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
146
191
|
if (!fs_1.default.existsSync(path_1.default.resolve(archiveName))) {
|
|
147
192
|
console.error("ARCHIVE NOT CREATED!");
|
|
148
193
|
return "";
|
|
@@ -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`);
|
|
@@ -14,8 +14,20 @@ function validateConfig(cfg) {
|
|
|
14
14
|
return "not an object";
|
|
15
15
|
if (!cfg.name || !cfg.token || !cfg.host || !cfg.username || !cfg.password)
|
|
16
16
|
return "missing one of: name, token, host, username, password";
|
|
17
|
-
if (cfg.includes
|
|
18
|
-
|
|
17
|
+
if (cfg.includes) {
|
|
18
|
+
if (!Array.isArray(cfg.includes))
|
|
19
|
+
return "includes must be an array";
|
|
20
|
+
for (const inc of cfg.includes) {
|
|
21
|
+
if (typeof inc === "string")
|
|
22
|
+
continue;
|
|
23
|
+
if (!inc || typeof inc !== "object")
|
|
24
|
+
return "includes entries must be a string or { src, dest? }";
|
|
25
|
+
if (typeof inc.src !== "string" || inc.src.length === 0)
|
|
26
|
+
return "includes entry is missing a non-empty 'src'";
|
|
27
|
+
if (inc.dest !== undefined && (typeof inc.dest !== "string" || inc.dest.length === 0))
|
|
28
|
+
return `includes entry "${inc.src}" has an invalid 'dest'`;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
19
31
|
if (cfg.excludes && !Array.isArray(cfg.excludes))
|
|
20
32
|
return "excludes must be an array";
|
|
21
33
|
if (cfg.switch_host && typeof cfg.switch_host !== "string")
|
|
@@ -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>;
|