badmfck-api-server 4.1.37 → 4.1.39
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/apiServer/APIService.js +2 -1
- package/dist/apiServer/deployment/Activate.d.ts +10 -0
- package/dist/apiServer/deployment/Activate.js +137 -0
- package/dist/apiServer/deployment/ConfigService.d.ts +15 -10
- package/dist/apiServer/deployment/ConfigService.js +6 -4
- package/dist/apiServer/deployment/Deploy.d.ts +2 -0
- package/dist/apiServer/deployment/Deploy.js +191 -18
- package/dist/apiServer/deployment/DeployerService.d.ts +14 -1
- package/dist/apiServer/deployment/DeployerService.js +141 -50
- package/dist/apiServer/deployment/NginxHelper.d.ts +7 -6
- package/dist/apiServer/deployment/NginxHelper.js +54 -26
- package/dist/apiServer/http/Http.js +14 -7
- package/dist/index.d.ts +2 -1
- package/dist/index.js +3 -1
- package/package.json +1 -1
|
@@ -249,10 +249,11 @@ class APIService extends BaseService_1.BaseService {
|
|
|
249
249
|
origin: (origin, callback) => {
|
|
250
250
|
if (!origin)
|
|
251
251
|
return callback(null, true);
|
|
252
|
+
if (this.noCors)
|
|
253
|
+
return callback(null, true);
|
|
252
254
|
try {
|
|
253
255
|
const o = new URL(String(origin));
|
|
254
256
|
const originNorm = o.origin.replace(/\/$/, "");
|
|
255
|
-
const hostNorm = o.host.replace(/\/$/, "");
|
|
256
257
|
const ok = corsSet.has(originNorm);
|
|
257
258
|
return callback(null, ok);
|
|
258
259
|
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
interface IActivateParams {
|
|
2
|
+
name: string;
|
|
3
|
+
token: string;
|
|
4
|
+
host: string;
|
|
5
|
+
username: string;
|
|
6
|
+
password: string;
|
|
7
|
+
switch_host?: string;
|
|
8
|
+
}
|
|
9
|
+
export declare function Activate(opt: IActivateParams | string, scheme?: "blue" | "green"): Promise<string>;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,137 @@
|
|
|
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.Activate = void 0;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
9
|
+
const __1 = require("../..");
|
|
10
|
+
async function Activate(opt, scheme) {
|
|
11
|
+
if (typeof opt === "string") {
|
|
12
|
+
const configPath = opt;
|
|
13
|
+
if (!fs_1.default.existsSync(configPath)) {
|
|
14
|
+
throw new Error(`Config file not found: ${configPath}`);
|
|
15
|
+
}
|
|
16
|
+
opt = JSON.parse(fs_1.default.readFileSync(configPath).toString("utf-8"));
|
|
17
|
+
if (!opt.name || !opt.token || !opt.host || !opt.username || !opt.password) {
|
|
18
|
+
throw new Error(`Invalid activate config file (missing required fields): ${configPath}`);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
if (scheme && scheme !== "blue" && scheme !== "green") {
|
|
22
|
+
throw new Error(`Invalid scheme: ${scheme}. Must be "blue" or "green" (or omitted).`);
|
|
23
|
+
}
|
|
24
|
+
let switchUrl;
|
|
25
|
+
if (opt.switch_host) {
|
|
26
|
+
switchUrl = opt.switch_host;
|
|
27
|
+
}
|
|
28
|
+
else if (/\/add\/?$/.test(opt.host)) {
|
|
29
|
+
switchUrl = opt.host.replace(/\/add\/?$/, "/switch");
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
throw new Error(`Cannot derive switch URL from host "${opt.host}". ` +
|
|
33
|
+
`Either add "switch_host" to config, or make sure "host" ends with /add.`);
|
|
34
|
+
}
|
|
35
|
+
const authToken = crypto_1.default.createHash("sha256").update(opt.username + ":" + opt.password).digest("hex");
|
|
36
|
+
const requestBody = { name: opt.name, token: opt.token };
|
|
37
|
+
if (scheme)
|
|
38
|
+
requestBody.scheme = scheme;
|
|
39
|
+
console.log(`Activating${scheme ? ` slot=${scheme}` : " (auto: lastDeployed)"} for ${opt.name} at ${switchUrl}`);
|
|
40
|
+
const startedAt = Date.now();
|
|
41
|
+
const response = await __1.Http.post(switchUrl, requestBody, {
|
|
42
|
+
headers: { authorization: `Bearer ${authToken}` },
|
|
43
|
+
timeoutMs: 60 * 1000,
|
|
44
|
+
retry: { enabled: false },
|
|
45
|
+
});
|
|
46
|
+
const elapsedMs = Date.now() - startedAt;
|
|
47
|
+
const useColor = process.stdout.isTTY && !process.env.NO_COLOR;
|
|
48
|
+
const c = (code, t) => useColor ? `\x1b[${code}m${t}\x1b[0m` : t;
|
|
49
|
+
const red = (t) => c("31", t);
|
|
50
|
+
const green = (t) => c("32", t);
|
|
51
|
+
const yellow = (t) => c("33", t);
|
|
52
|
+
const dim = (t) => c("2", t);
|
|
53
|
+
const bold = (t) => c("1", t);
|
|
54
|
+
const line = dim("─".repeat(60));
|
|
55
|
+
const now = new Date();
|
|
56
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
57
|
+
const stamp = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`;
|
|
58
|
+
console.log("");
|
|
59
|
+
if (!response.ok) {
|
|
60
|
+
const statusStr = response.status !== undefined
|
|
61
|
+
? `HTTP ${response.status}`
|
|
62
|
+
: "network error (no response)";
|
|
63
|
+
console.error(red(bold("✗ ACTIVATE FAILED — transport error")));
|
|
64
|
+
console.error(line);
|
|
65
|
+
console.error(` ${dim("host: ")} ${switchUrl}`);
|
|
66
|
+
console.error(` ${dim("status: ")} ${statusStr}`);
|
|
67
|
+
console.error(` ${dim("elapsed: ")} ${elapsedMs}ms`);
|
|
68
|
+
if (response.error !== undefined) {
|
|
69
|
+
const errText = typeof response.error === "string"
|
|
70
|
+
? response.error
|
|
71
|
+
: JSON.stringify(response.error);
|
|
72
|
+
const clipped = errText.length > 400 ? errText.slice(0, 400) + `... (${errText.length} total)` : errText;
|
|
73
|
+
console.error(` ${dim("error: ")} ${clipped}`);
|
|
74
|
+
}
|
|
75
|
+
console.error(line);
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
const body = response.data;
|
|
79
|
+
let appError = null;
|
|
80
|
+
if (body && typeof body === "object") {
|
|
81
|
+
if (__1.ErrorUtils.isError(body?.error))
|
|
82
|
+
appError = body.error;
|
|
83
|
+
else if (__1.ErrorUtils.isError(body?.data?.error))
|
|
84
|
+
appError = body.data.error;
|
|
85
|
+
else if (__1.ErrorUtils.isError(body?.data))
|
|
86
|
+
appError = body.data;
|
|
87
|
+
else if (__1.ErrorUtils.isError(body))
|
|
88
|
+
appError = body;
|
|
89
|
+
}
|
|
90
|
+
if (appError) {
|
|
91
|
+
console.error(red(bold("✗ ACTIVATE REJECTED BY SERVER")));
|
|
92
|
+
console.error(line);
|
|
93
|
+
console.error(` ${dim("host: ")} ${switchUrl}`);
|
|
94
|
+
console.error(` ${dim("code: ")} ${appError.code}`);
|
|
95
|
+
console.error(` ${dim("message: ")} ${appError.message}`);
|
|
96
|
+
if (appError.httpStatus)
|
|
97
|
+
console.error(` ${dim("httpCode: ")} ${appError.httpStatus}`);
|
|
98
|
+
if (appError.details !== undefined) {
|
|
99
|
+
const d = typeof appError.details === "string" ? appError.details : JSON.stringify(appError.details);
|
|
100
|
+
const clipped = d.length > 400 ? d.slice(0, 400) + `... (${d.length} total)` : d;
|
|
101
|
+
console.error(` ${dim("details: ")} ${clipped}`);
|
|
102
|
+
}
|
|
103
|
+
console.error(` ${dim("elapsed: ")} ${elapsedMs}ms`);
|
|
104
|
+
console.error(line);
|
|
105
|
+
process.exit(1);
|
|
106
|
+
}
|
|
107
|
+
const info = body?.data ?? {};
|
|
108
|
+
const action = info.action ?? "switch";
|
|
109
|
+
const active = info.active ?? "?";
|
|
110
|
+
const previous = info.previousActive ?? "?";
|
|
111
|
+
const hookError = info.hookError ?? null;
|
|
112
|
+
if (hookError) {
|
|
113
|
+
console.log(yellow(bold("⚠ SLOT FLIPPED BUT nginx RELOAD FAILED")));
|
|
114
|
+
}
|
|
115
|
+
else if (action === "rollback") {
|
|
116
|
+
console.log(green(bold("✓ ROLLBACK SUCCESSFUL")));
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
console.log(green(bold("✓ ACTIVATE SUCCESSFUL")));
|
|
120
|
+
}
|
|
121
|
+
console.log(line);
|
|
122
|
+
console.log(` ${dim("project: ")} ${opt.name}`);
|
|
123
|
+
console.log(` ${dim("action: ")} ${action}`);
|
|
124
|
+
console.log(` ${dim("active: ")} ${previous} ${dim("→")} ${bold(active)}`);
|
|
125
|
+
console.log(` ${dim("host: ")} ${switchUrl}`);
|
|
126
|
+
console.log(` ${dim("status: ")} HTTP ${response.status}`);
|
|
127
|
+
console.log(` ${dim("elapsed: ")} ${elapsedMs}ms`);
|
|
128
|
+
console.log(` ${dim("time: ")} ${stamp}`);
|
|
129
|
+
if (hookError) {
|
|
130
|
+
console.log(` ${dim("hookErr: ")} ${yellow(hookError)}`);
|
|
131
|
+
console.log(dim(` (state + symlink already committed; retry the reload manually: nginx -s reload)`));
|
|
132
|
+
}
|
|
133
|
+
console.log(line);
|
|
134
|
+
console.log("");
|
|
135
|
+
return active;
|
|
136
|
+
}
|
|
137
|
+
exports.Activate = Activate;
|
|
@@ -49,10 +49,12 @@ declare const _TConfig: {
|
|
|
49
49
|
};
|
|
50
50
|
readonly $__bluegreen_optional: true;
|
|
51
51
|
readonly nginx: {
|
|
52
|
-
readonly
|
|
53
|
-
readonly $
|
|
54
|
-
readonly
|
|
55
|
-
readonly $
|
|
52
|
+
readonly config_dir: "";
|
|
53
|
+
readonly $__config_dir_optional: true;
|
|
54
|
+
readonly domain: "";
|
|
55
|
+
readonly $__domain_optional: true;
|
|
56
|
+
readonly upstream_name: "";
|
|
57
|
+
readonly $__upstream_name_optional: true;
|
|
56
58
|
};
|
|
57
59
|
readonly $__nginx_optional: true;
|
|
58
60
|
};
|
|
@@ -69,8 +71,9 @@ export declare const REQ_DEPLOYMENT_CONFIG: Req<void, Map<string, {
|
|
|
69
71
|
green_config: {};
|
|
70
72
|
} | undefined;
|
|
71
73
|
nginx?: {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
+
config_dir?: string | undefined;
|
|
75
|
+
domain?: string | undefined;
|
|
76
|
+
upstream_name?: string | undefined;
|
|
74
77
|
} | undefined;
|
|
75
78
|
name: string;
|
|
76
79
|
destination: string;
|
|
@@ -98,8 +101,9 @@ export declare const REQ_DEPLOYMENT_CONFIG_ADD: Req<{
|
|
|
98
101
|
green_config: {};
|
|
99
102
|
} | undefined;
|
|
100
103
|
nginx?: {
|
|
101
|
-
|
|
102
|
-
|
|
104
|
+
config_dir?: string | undefined;
|
|
105
|
+
domain?: string | undefined;
|
|
106
|
+
upstream_name?: string | undefined;
|
|
103
107
|
} | undefined;
|
|
104
108
|
name: string;
|
|
105
109
|
destination: string;
|
|
@@ -126,8 +130,9 @@ export declare const REQ_DEPLOYMENT_CONFIG_ADD: Req<{
|
|
|
126
130
|
green_config: {};
|
|
127
131
|
} | undefined;
|
|
128
132
|
nginx?: {
|
|
129
|
-
|
|
130
|
-
|
|
133
|
+
config_dir?: string | undefined;
|
|
134
|
+
domain?: string | undefined;
|
|
135
|
+
upstream_name?: string | undefined;
|
|
131
136
|
} | undefined;
|
|
132
137
|
name: string;
|
|
133
138
|
destination: string;
|
|
@@ -80,10 +80,12 @@ const _TConfig = {
|
|
|
80
80
|
},
|
|
81
81
|
$__bluegreen_optional: true,
|
|
82
82
|
nginx: {
|
|
83
|
-
|
|
84
|
-
$
|
|
85
|
-
|
|
86
|
-
$
|
|
83
|
+
config_dir: "",
|
|
84
|
+
$__config_dir_optional: true,
|
|
85
|
+
domain: "",
|
|
86
|
+
$__domain_optional: true,
|
|
87
|
+
upstream_name: "",
|
|
88
|
+
$__upstream_name_optional: true,
|
|
87
89
|
},
|
|
88
90
|
$__nginx_optional: true,
|
|
89
91
|
};
|
|
@@ -18,23 +18,44 @@ async function Deploy(opt) {
|
|
|
18
18
|
if (!opt.name || !opt.token || !opt.host || !opt.username || !opt.password) {
|
|
19
19
|
throw new Error(`Invalid deploy config file: ${opt}`);
|
|
20
20
|
}
|
|
21
|
+
if (opt.includes && !Array.isArray(opt.includes)) {
|
|
22
|
+
throw new Error(`Invalid deploy config file: includes must be an array`);
|
|
23
|
+
}
|
|
24
|
+
if (opt.excludes && !Array.isArray(opt.excludes)) {
|
|
25
|
+
throw new Error(`Invalid deploy config file: excludes must be an array`);
|
|
26
|
+
}
|
|
21
27
|
}
|
|
22
28
|
const archiveName = __1.UID.sha256(opt.name.replaceAll(".", "_")) + ".tar.gz";
|
|
23
29
|
console.log("Changing Config to live");
|
|
24
30
|
const config = path_1.default.resolve("src", "Config.ts");
|
|
25
31
|
let configSrc = fs_1.default.readFileSync(config).toString("utf-8");
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
32
|
+
const schemeRe = /(\bscheme\s*:\s*Scheme\s*=\s*)local\b/g;
|
|
33
|
+
let replaced = 0;
|
|
34
|
+
const updated = configSrc.replace(schemeRe, (_match, prefix) => {
|
|
35
|
+
replaced++;
|
|
36
|
+
return prefix + "live";
|
|
37
|
+
});
|
|
38
|
+
if (replaced > 0) {
|
|
39
|
+
fs_1.default.writeFileSync(config, updated);
|
|
40
|
+
console.log(`Config: switched to live (${replaced} occurrence${replaced === 1 ? "" : "s"})`);
|
|
41
|
+
}
|
|
42
|
+
else if (/\bscheme\s*:\s*Scheme\s*=\s*live\b/.test(configSrc)) {
|
|
43
|
+
console.log("Config already in live mode — nothing to change");
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
console.warn("Config: no 'scheme: Scheme = local|live' pattern found in " + config +
|
|
47
|
+
" — check the file was not renamed/restructured");
|
|
29
48
|
}
|
|
30
49
|
const pkgPath = path_1.default.resolve("package.json");
|
|
31
50
|
const pkg = JSON.parse(fs_1.default.readFileSync(pkgPath).toString("utf-8"));
|
|
32
|
-
const
|
|
51
|
+
const oldVersion = String(pkg.version || "0.0.0");
|
|
52
|
+
const pkgName = String(pkg.name || opt.name);
|
|
53
|
+
const parts = oldVersion.split(".").map(p => parseInt(p, 10) || 0);
|
|
33
54
|
while (parts.length < 3)
|
|
34
55
|
parts.push(0);
|
|
35
56
|
parts[parts.length - 1] += 1;
|
|
36
57
|
const newVersion = parts.join(".");
|
|
37
|
-
console.log(`Bumping version: ${
|
|
58
|
+
console.log(`Bumping version: ${oldVersion} -> ${newVersion}`);
|
|
38
59
|
pkg.version = newVersion;
|
|
39
60
|
fs_1.default.writeFileSync(pkgPath, JSON.stringify(pkg, null, "\t") + "\n");
|
|
40
61
|
if (fs_1.default.existsSync(path_1.default.resolve(archiveName))) {
|
|
@@ -44,26 +65,178 @@ async function Deploy(opt) {
|
|
|
44
65
|
console.log("Build");
|
|
45
66
|
var execResult = run("npm run build");
|
|
46
67
|
console.log(execResult);
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
68
|
+
const items = ["./bin", "package.json"];
|
|
69
|
+
if (Array.isArray(opt.includes)) {
|
|
70
|
+
for (const inc of opt.includes) {
|
|
71
|
+
if (typeof inc !== "string" || inc.length === 0)
|
|
72
|
+
continue;
|
|
73
|
+
if (inc.startsWith("-")) {
|
|
74
|
+
console.warn(`Skipping include that starts with '-': ${inc}`);
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (!fs_1.default.existsSync(inc)) {
|
|
78
|
+
console.warn(`Include path does not exist (tar will error): ${inc}`);
|
|
79
|
+
}
|
|
80
|
+
items.push(inc);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
const excludes = new Set();
|
|
84
|
+
const addExclude = (raw, source) => {
|
|
85
|
+
if (typeof raw !== "string")
|
|
86
|
+
return;
|
|
87
|
+
const trimmed = raw.trim();
|
|
88
|
+
if (trimmed.length === 0 || trimmed.startsWith("#"))
|
|
89
|
+
return;
|
|
90
|
+
if (trimmed.startsWith("-")) {
|
|
91
|
+
console.warn(`Skipping exclude starting with '-' from ${source}: ${trimmed}`);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
excludes.add(trimmed);
|
|
95
|
+
};
|
|
96
|
+
const ignoreFile = path_1.default.resolve(".deployignore");
|
|
97
|
+
if (fs_1.default.existsSync(ignoreFile)) {
|
|
98
|
+
const lines = fs_1.default.readFileSync(ignoreFile, "utf-8").split(/\r?\n/);
|
|
99
|
+
for (const line of lines)
|
|
100
|
+
addExclude(line, ".deployignore");
|
|
101
|
+
}
|
|
102
|
+
if (Array.isArray(opt.excludes)) {
|
|
103
|
+
for (const ex of opt.excludes)
|
|
104
|
+
addExclude(ex, "config.excludes");
|
|
105
|
+
}
|
|
106
|
+
const excludeArgs = Array.from(excludes).map(p => `--exclude=${p}`);
|
|
107
|
+
console.log("Archiving:", items.join(", "));
|
|
108
|
+
if (excludeArgs.length > 0)
|
|
109
|
+
console.log("Excluding:", Array.from(excludes).join(", "));
|
|
110
|
+
try {
|
|
111
|
+
(0, child_process_1.execFileSync)("tar", ["-czvf", archiveName, ...excludeArgs, ...items], { encoding: "utf-8", stdio: "inherit" });
|
|
112
|
+
}
|
|
113
|
+
catch (err) {
|
|
114
|
+
console.error(`\ntar failed while creating ${archiveName}\n`);
|
|
115
|
+
const out = (err.stdout || "").toString().trim();
|
|
116
|
+
const errOut = (err.stderr || "").toString().trim();
|
|
117
|
+
if (out)
|
|
118
|
+
console.error(out);
|
|
119
|
+
if (errOut)
|
|
120
|
+
console.error(errOut);
|
|
121
|
+
process.exit(typeof err.status === "number" ? err.status : 1);
|
|
122
|
+
}
|
|
50
123
|
if (!fs_1.default.existsSync(path_1.default.resolve(archiveName))) {
|
|
51
124
|
console.error("ARCHIVE NOT CREATED!");
|
|
52
125
|
return "";
|
|
53
126
|
}
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
"
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
127
|
+
const authToken = crypto_1.default.createHash("sha256").update(opt.username + ":" + opt.password).digest("hex");
|
|
128
|
+
const archivePath = path_1.default.resolve(archiveName);
|
|
129
|
+
const fileBlob = await fs_1.default.openAsBlob(archivePath);
|
|
130
|
+
const formData = new FormData();
|
|
131
|
+
formData.set("token", opt.token);
|
|
132
|
+
formData.set("name", opt.name);
|
|
133
|
+
formData.set("file", fileBlob, archiveName);
|
|
134
|
+
console.log(`Deploying to ${opt.host} (archive: ${archiveName})`);
|
|
135
|
+
const startedAt = Date.now();
|
|
136
|
+
const uploadResponse = await __1.Http.post(opt.host, formData, {
|
|
137
|
+
headers: { authorization: `Bearer ${authToken}` },
|
|
138
|
+
timeoutMs: 5 * 60 * 1000,
|
|
139
|
+
retry: { enabled: false },
|
|
140
|
+
});
|
|
141
|
+
const elapsedMs = Date.now() - startedAt;
|
|
142
|
+
const useColor = process.stdout.isTTY && !process.env.NO_COLOR;
|
|
143
|
+
const c = (code, t) => useColor ? `\x1b[${code}m${t}\x1b[0m` : t;
|
|
144
|
+
const red = (t) => c("31", t);
|
|
145
|
+
const green = (t) => c("32", t);
|
|
146
|
+
const yellow = (t) => c("33", t);
|
|
147
|
+
const dim = (t) => c("2", t);
|
|
148
|
+
const bold = (t) => c("1", t);
|
|
149
|
+
const line = dim("─".repeat(60));
|
|
63
150
|
const deployedAt = new Date();
|
|
64
151
|
const pad = (n) => String(n).padStart(2, "0");
|
|
65
152
|
const stamp = `${deployedAt.getFullYear()}-${pad(deployedAt.getMonth() + 1)}-${pad(deployedAt.getDate())} ${pad(deployedAt.getHours())}:${pad(deployedAt.getMinutes())}:${pad(deployedAt.getSeconds())}`;
|
|
66
|
-
console.log(
|
|
153
|
+
console.log("");
|
|
154
|
+
if (!uploadResponse.ok) {
|
|
155
|
+
const statusStr = uploadResponse.status !== undefined
|
|
156
|
+
? `HTTP ${uploadResponse.status}`
|
|
157
|
+
: "network error (no response)";
|
|
158
|
+
console.error(red(bold("✗ DEPLOY FAILED — transport error")));
|
|
159
|
+
console.error(line);
|
|
160
|
+
console.error(` ${dim("host: ")} ${opt.host}`);
|
|
161
|
+
console.error(` ${dim("status: ")} ${statusStr}`);
|
|
162
|
+
console.error(` ${dim("elapsed: ")} ${elapsedMs}ms`);
|
|
163
|
+
if (uploadResponse.error !== undefined) {
|
|
164
|
+
const errText = typeof uploadResponse.error === "string"
|
|
165
|
+
? uploadResponse.error
|
|
166
|
+
: JSON.stringify(uploadResponse.error);
|
|
167
|
+
const clipped = errText.length > 400 ? errText.slice(0, 400) + `... (${errText.length} total)` : errText;
|
|
168
|
+
console.error(` ${dim("error: ")} ${clipped}`);
|
|
169
|
+
}
|
|
170
|
+
console.error(line);
|
|
171
|
+
process.exit(1);
|
|
172
|
+
}
|
|
173
|
+
const body = uploadResponse.data;
|
|
174
|
+
let appError = null;
|
|
175
|
+
if (body && typeof body === "object") {
|
|
176
|
+
if (__1.ErrorUtils.isError(body?.error))
|
|
177
|
+
appError = body.error;
|
|
178
|
+
else if (__1.ErrorUtils.isError(body?.data?.error))
|
|
179
|
+
appError = body.data.error;
|
|
180
|
+
else if (__1.ErrorUtils.isError(body?.data))
|
|
181
|
+
appError = body.data;
|
|
182
|
+
else if (__1.ErrorUtils.isError(body))
|
|
183
|
+
appError = body;
|
|
184
|
+
}
|
|
185
|
+
if (appError) {
|
|
186
|
+
console.error(red(bold("✗ DEPLOY REJECTED BY SERVER")));
|
|
187
|
+
console.error(line);
|
|
188
|
+
console.error(` ${dim("host: ")} ${opt.host}`);
|
|
189
|
+
console.error(` ${dim("code: ")} ${appError.code}`);
|
|
190
|
+
console.error(` ${dim("message: ")} ${appError.message}`);
|
|
191
|
+
if (appError.httpStatus)
|
|
192
|
+
console.error(` ${dim("httpCode: ")} ${appError.httpStatus}`);
|
|
193
|
+
if (appError.details !== undefined) {
|
|
194
|
+
const d = typeof appError.details === "string" ? appError.details : JSON.stringify(appError.details);
|
|
195
|
+
const clipped = d.length > 400 ? d.slice(0, 400) + `... (${d.length} total)` : d;
|
|
196
|
+
console.error(` ${dim("details: ")} ${clipped}`);
|
|
197
|
+
}
|
|
198
|
+
console.error(` ${dim("elapsed: ")} ${elapsedMs}ms`);
|
|
199
|
+
console.error(line);
|
|
200
|
+
process.exit(1);
|
|
201
|
+
}
|
|
202
|
+
const emptyResponse = body === null || body === undefined
|
|
203
|
+
|| (typeof body === "object" && Object.keys(body).length === 0);
|
|
204
|
+
if (emptyResponse) {
|
|
205
|
+
console.log(yellow(bold("⚠ DEPLOY LIKELY OK (server returned empty response)")));
|
|
206
|
+
}
|
|
207
|
+
else {
|
|
208
|
+
console.log(green(bold("✓ DEPLOY SUCCESSFUL")));
|
|
209
|
+
}
|
|
210
|
+
console.log(line);
|
|
211
|
+
console.log(` ${dim("project: ")} ${opt.name}`);
|
|
212
|
+
console.log(` ${dim("package: ")} ${pkgName}`);
|
|
213
|
+
console.log(` ${dim("version: ")} ${oldVersion} ${dim("→")} ${bold(newVersion)}`);
|
|
214
|
+
console.log(` ${dim("archive: ")} ${archiveName}`);
|
|
215
|
+
console.log(` ${dim("host: ")} ${opt.host}`);
|
|
216
|
+
console.log(` ${dim("status: ")} HTTP ${uploadResponse.status}`);
|
|
217
|
+
console.log(` ${dim("elapsed: ")} ${elapsedMs}ms`);
|
|
218
|
+
console.log(` ${dim("time: ")} ${stamp}`);
|
|
219
|
+
const info = body?.data?.data;
|
|
220
|
+
if (info && typeof info === "object" && info.bluegreen === true) {
|
|
221
|
+
const slotBadge = info.activeThisDeploy
|
|
222
|
+
? green("← active in production")
|
|
223
|
+
: yellow("(inactive — deploy landed but nginx still points at " + info.active + "; run /pckg/switch to promote)");
|
|
224
|
+
console.log(` ${dim("mode: ")} blue-green`);
|
|
225
|
+
console.log(` ${dim("slot: ")} ${bold(String(info.slot))} ${slotBadge}`);
|
|
226
|
+
if (info.active && info.active !== info.slot) {
|
|
227
|
+
console.log(` ${dim("active: ")} ${info.active}`);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
else if (info && typeof info === "object" && info.bluegreen === false) {
|
|
231
|
+
console.log(` ${dim("mode: ")} single-instance`);
|
|
232
|
+
}
|
|
233
|
+
if (!emptyResponse) {
|
|
234
|
+
const payload = JSON.stringify(body);
|
|
235
|
+
const clipped = payload.length > 300 ? payload.slice(0, 300) + `... (${payload.length} total)` : payload;
|
|
236
|
+
console.log(` ${dim("response:")} ${clipped}`);
|
|
237
|
+
}
|
|
238
|
+
console.log(line);
|
|
239
|
+
console.log("");
|
|
67
240
|
return archiveName;
|
|
68
241
|
}
|
|
69
242
|
exports.Deploy = Deploy;
|
|
@@ -46,8 +46,13 @@ export declare class DeployerService extends BaseService {
|
|
|
46
46
|
setup: TDeployerSetup | null;
|
|
47
47
|
notifier: IDeploymentNotifier | null;
|
|
48
48
|
busy: Map<string, boolean>;
|
|
49
|
+
private _setupWatchDebounceTimer;
|
|
50
|
+
private _lastSetupHash;
|
|
49
51
|
constructor(options: IDeployerServiceOptions);
|
|
50
52
|
init(): Promise<void>;
|
|
53
|
+
private _loadSetup;
|
|
54
|
+
private _reloadSetup;
|
|
55
|
+
private _watchSetup;
|
|
51
56
|
proceed(data: {
|
|
52
57
|
auth?: string;
|
|
53
58
|
name: string;
|
|
@@ -63,7 +68,14 @@ export declare class DeployerService extends BaseService {
|
|
|
63
68
|
};
|
|
64
69
|
data?: undefined;
|
|
65
70
|
} | {
|
|
66
|
-
data:
|
|
71
|
+
data: {
|
|
72
|
+
ok: boolean;
|
|
73
|
+
project: string;
|
|
74
|
+
bluegreen: boolean;
|
|
75
|
+
slot: "blue" | "green" | null;
|
|
76
|
+
active: "blue" | "green" | null;
|
|
77
|
+
activeThisDeploy: boolean;
|
|
78
|
+
};
|
|
67
79
|
error?: undefined;
|
|
68
80
|
}>;
|
|
69
81
|
switchSlot(data: {
|
|
@@ -107,6 +119,7 @@ export declare class DeployerService extends BaseService {
|
|
|
107
119
|
}>;
|
|
108
120
|
tokensMatch(a: string | undefined, b: string | undefined): boolean;
|
|
109
121
|
private _checkUserAuth;
|
|
122
|
+
private _renderNginx;
|
|
110
123
|
runPM2(found: IConfig, projectState?: IProjectState | null): Promise<IError | null>;
|
|
111
124
|
private static readonly SKIP_DIRS;
|
|
112
125
|
private static readonly TEMPLATE_EXTENSIONS;
|
|
@@ -37,6 +37,8 @@ class DeployerService extends BaseService_1.BaseService {
|
|
|
37
37
|
setup = null;
|
|
38
38
|
notifier = null;
|
|
39
39
|
busy = new Map();
|
|
40
|
+
_setupWatchDebounceTimer = null;
|
|
41
|
+
_lastSetupHash = null;
|
|
40
42
|
constructor(options) {
|
|
41
43
|
super("DeployerService");
|
|
42
44
|
this.options = options;
|
|
@@ -45,41 +47,84 @@ class DeployerService extends BaseService_1.BaseService {
|
|
|
45
47
|
super.init();
|
|
46
48
|
const cs = new ConfigService_1.ConfigService(this.options.projects_path);
|
|
47
49
|
await cs.init();
|
|
50
|
+
const initial = await this._loadSetup();
|
|
51
|
+
if (!initial) {
|
|
52
|
+
throw new Error("Setup file missing/invalid at start — cannot start deployer");
|
|
53
|
+
}
|
|
54
|
+
this.setup = initial;
|
|
55
|
+
this._lastSetupHash = crypto_1.default.createHash("md5").update(JSON.stringify(initial)).digest("hex");
|
|
56
|
+
if (this.setup.notifier?.URL && this.setup.notifier?.KEY) {
|
|
57
|
+
this.notifier = new Notifier_1.Notifier({ URL: this.setup.notifier.URL, KEY: this.setup.notifier.KEY });
|
|
58
|
+
await this.notifier.init();
|
|
59
|
+
}
|
|
60
|
+
if (this.setup.watchdog) {
|
|
61
|
+
const wd = new Watchdog_1.Watchdog(this.notifier);
|
|
62
|
+
await wd.init();
|
|
63
|
+
}
|
|
64
|
+
this._watchSetup();
|
|
65
|
+
exports.REQ_DEPLOYMENT_PROCEED.listener = async (data) => this.proceed(data);
|
|
66
|
+
exports.REQ_DEPLOYMENT_SWITCH.listener = async (data) => this.switchSlot(data);
|
|
67
|
+
exports.REQ_DEPLOYMENT_STATUS.listener = async (data) => this.status(data);
|
|
68
|
+
}
|
|
69
|
+
async _loadSetup() {
|
|
48
70
|
const setupFile = path_1.default.resolve(this.options.setup_path, "setup.json");
|
|
49
71
|
if (!fs_1.default.existsSync(setupFile)) {
|
|
50
72
|
(0, LogService_1.logError)("Setup file not found: " + setupFile);
|
|
51
|
-
|
|
73
|
+
return null;
|
|
52
74
|
}
|
|
75
|
+
let raw;
|
|
53
76
|
try {
|
|
54
|
-
|
|
77
|
+
raw = fs_1.default.readFileSync(setupFile, "utf-8");
|
|
55
78
|
}
|
|
56
79
|
catch (e) {
|
|
57
80
|
(0, LogService_1.logError)("Failed to read setup file: " + e.message);
|
|
58
|
-
|
|
81
|
+
return null;
|
|
59
82
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
throw new Error("Setup file validation failed: " + JSON.stringify(validation));
|
|
83
|
+
let parsed;
|
|
84
|
+
try {
|
|
85
|
+
parsed = JSON.parse(raw);
|
|
64
86
|
}
|
|
65
|
-
|
|
66
|
-
(0, LogService_1.logError)("
|
|
67
|
-
|
|
87
|
+
catch (e) {
|
|
88
|
+
(0, LogService_1.logError)("Failed to parse setup.json: " + e.message);
|
|
89
|
+
return null;
|
|
68
90
|
}
|
|
69
|
-
|
|
70
|
-
if (
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
await notifier.init();
|
|
74
|
-
}
|
|
91
|
+
const errors = await __1.Validator.validateStructure(_TSetup, parsed);
|
|
92
|
+
if (errors && errors.length > 0) {
|
|
93
|
+
(0, LogService_1.logError)("Setup validation failed: " + JSON.stringify(errors));
|
|
94
|
+
return null;
|
|
75
95
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
96
|
+
return parsed;
|
|
97
|
+
}
|
|
98
|
+
async _reloadSetup() {
|
|
99
|
+
const fresh = await this._loadSetup();
|
|
100
|
+
if (!fresh) {
|
|
101
|
+
(0, LogService_1.logWarn)("Setup reload failed — keeping previous good setup in memory");
|
|
102
|
+
return;
|
|
79
103
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
104
|
+
const hash = crypto_1.default.createHash("md5").update(JSON.stringify(fresh)).digest("hex");
|
|
105
|
+
if (hash === this._lastSetupHash)
|
|
106
|
+
return;
|
|
107
|
+
this._lastSetupHash = hash;
|
|
108
|
+
this.setup = fresh;
|
|
109
|
+
(0, LogService_1.logWarn)("Setup reloaded from disk (users list refreshed live; notifier/watchdog need service restart to change)");
|
|
110
|
+
}
|
|
111
|
+
_watchSetup() {
|
|
112
|
+
try {
|
|
113
|
+
fs_1.default.watch(this.options.setup_path, (_eventType, filename) => {
|
|
114
|
+
if (filename !== "setup.json")
|
|
115
|
+
return;
|
|
116
|
+
if (this._setupWatchDebounceTimer)
|
|
117
|
+
clearTimeout(this._setupWatchDebounceTimer);
|
|
118
|
+
this._setupWatchDebounceTimer = setTimeout(() => {
|
|
119
|
+
this._setupWatchDebounceTimer = null;
|
|
120
|
+
this._reloadSetup();
|
|
121
|
+
}, 200);
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
catch (e) {
|
|
125
|
+
(0, LogService_1.logError)("Failed to fs.watch setup file: " + e.message);
|
|
126
|
+
}
|
|
127
|
+
setInterval(() => { this._reloadSetup(); }, 60_000).unref();
|
|
83
128
|
}
|
|
84
129
|
async proceed(data) {
|
|
85
130
|
const user = this._checkUserAuth(data.auth);
|
|
@@ -109,6 +154,7 @@ class DeployerService extends BaseService_1.BaseService {
|
|
|
109
154
|
const safeName = (path_1.default.basename(file.name || "").replace(/[^a-zA-Z0-9._-]/g, "_")) || "upload";
|
|
110
155
|
let projectState = null;
|
|
111
156
|
let projectStatePath = null;
|
|
157
|
+
let projectStateWasCreated = false;
|
|
112
158
|
if (found.bluegreen && found.bluegreen.blue_config && found.bluegreen.green_config) {
|
|
113
159
|
projectStatePath = path_1.default.resolve(this.options.projects_path, data.name + "_state_.json");
|
|
114
160
|
if (fs_1.default.existsSync(projectStatePath)) {
|
|
@@ -125,6 +171,7 @@ class DeployerService extends BaseService_1.BaseService {
|
|
|
125
171
|
lastDeployed: "blue",
|
|
126
172
|
history: []
|
|
127
173
|
};
|
|
174
|
+
projectStateWasCreated = true;
|
|
128
175
|
}
|
|
129
176
|
else {
|
|
130
177
|
projectState.lastDeployed = projectState.active === "blue" ? "green" : "blue";
|
|
@@ -186,37 +233,31 @@ class DeployerService extends BaseService_1.BaseService {
|
|
|
186
233
|
if (found.config) {
|
|
187
234
|
await this.applyConfig(found.destination, found.config);
|
|
188
235
|
}
|
|
189
|
-
if (found.
|
|
190
|
-
const
|
|
191
|
-
if (
|
|
192
|
-
if (found.email && found.email.length > 0) {
|
|
193
|
-
for (let email of found.email) {
|
|
194
|
-
this.notifier?.notifyEmailStatus(email, "error-nginx-missing", found.name, user.login);
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
throw { code: 22, message: "nginx config declared but file not found: " + nginxPath, httpStatus: 500 };
|
|
198
|
-
}
|
|
199
|
-
try {
|
|
200
|
-
NginxHelper_1.NginxHelper.applyTemplate(nginxPath, found.config ?? {});
|
|
201
|
-
}
|
|
202
|
-
catch (e) {
|
|
236
|
+
if (found.pm2) {
|
|
237
|
+
const pm2Result = await this.runPM2(found, projectState);
|
|
238
|
+
if (__1.ErrorUtils.isError(pm2Result)) {
|
|
203
239
|
if (found.email && found.email.length > 0) {
|
|
204
240
|
for (let email of found.email) {
|
|
205
|
-
this.notifier?.notifyEmailStatus(email, "error-
|
|
241
|
+
this.notifier?.notifyEmailStatus(email, "error-pm2", found.name, user.login);
|
|
206
242
|
}
|
|
207
243
|
}
|
|
208
|
-
throw
|
|
244
|
+
throw pm2Result;
|
|
209
245
|
}
|
|
210
246
|
}
|
|
211
|
-
if (found.
|
|
212
|
-
const
|
|
213
|
-
|
|
247
|
+
if (found.nginx?.config_dir && (!projectState || projectStateWasCreated)) {
|
|
248
|
+
const activeCfg = (found.config ?? {});
|
|
249
|
+
const nginxErr = await this._renderNginx(found, data.name, activeCfg.PORT ?? "");
|
|
250
|
+
if (nginxErr) {
|
|
214
251
|
if (found.email && found.email.length > 0) {
|
|
215
252
|
for (let email of found.email) {
|
|
216
|
-
this.notifier?.notifyEmailStatus(email, "error-
|
|
253
|
+
this.notifier?.notifyEmailStatus(email, "error-nginx", found.name, user.login);
|
|
217
254
|
}
|
|
218
255
|
}
|
|
219
|
-
throw
|
|
256
|
+
throw nginxErr;
|
|
257
|
+
}
|
|
258
|
+
const hookErr = await NginxHelper_1.NginxHelper.runHook(found.bluegreen?.on_switch);
|
|
259
|
+
if (hookErr) {
|
|
260
|
+
(0, LogService_1.logWarn)("nginx reload after initial deploy failed: " + hookErr);
|
|
220
261
|
}
|
|
221
262
|
}
|
|
222
263
|
if (projectState && projectStatePath) {
|
|
@@ -249,7 +290,16 @@ class DeployerService extends BaseService_1.BaseService {
|
|
|
249
290
|
}
|
|
250
291
|
}, 5000);
|
|
251
292
|
}
|
|
252
|
-
return { data:
|
|
293
|
+
return { data: {
|
|
294
|
+
ok: true,
|
|
295
|
+
project: realName,
|
|
296
|
+
bluegreen: !!projectState,
|
|
297
|
+
slot: projectState?.lastDeployed ?? null,
|
|
298
|
+
active: projectState?.active ?? null,
|
|
299
|
+
activeThisDeploy: projectState
|
|
300
|
+
? projectState.active === projectState.lastDeployed
|
|
301
|
+
: true,
|
|
302
|
+
} };
|
|
253
303
|
}
|
|
254
304
|
finally {
|
|
255
305
|
this.busy.set(realName, false);
|
|
@@ -313,14 +363,18 @@ class DeployerService extends BaseService_1.BaseService {
|
|
|
313
363
|
fs_1.default.writeFileSync(tmp, JSON.stringify(projectState, null, 4), "utf-8");
|
|
314
364
|
fs_1.default.renameSync(tmp, projectStatePath);
|
|
315
365
|
let hookError = null;
|
|
316
|
-
if (found.nginx?.
|
|
317
|
-
const
|
|
318
|
-
const
|
|
319
|
-
|
|
320
|
-
|
|
366
|
+
if (found.nginx?.config_dir) {
|
|
367
|
+
const slotCfg = target === "blue" ? found.bluegreen.blue_config : found.bluegreen.green_config;
|
|
368
|
+
const merged = { ...(found.config ?? {}), ...(slotCfg ?? {}) };
|
|
369
|
+
const nginxErr = await this._renderNginx(found, data.name, merged.PORT ?? "");
|
|
370
|
+
if (nginxErr) {
|
|
371
|
+
(0, LogService_1.logError)("nginx render during switch failed (state already flipped): " + nginxErr.message);
|
|
372
|
+
hookError = nginxErr.message;
|
|
321
373
|
}
|
|
322
374
|
}
|
|
323
|
-
|
|
375
|
+
if (!hookError) {
|
|
376
|
+
hookError = await NginxHelper_1.NginxHelper.runHook(found.bluegreen.on_switch);
|
|
377
|
+
}
|
|
324
378
|
const status = hookError ? "switch-hook-failed" : action === "rollback" ? `rolled-back-to-${target}` : `switched-to-${target}`;
|
|
325
379
|
if (found.email && found.email.length > 0) {
|
|
326
380
|
for (const email of found.email) {
|
|
@@ -387,6 +441,43 @@ class DeployerService extends BaseService_1.BaseService {
|
|
|
387
441
|
}
|
|
388
442
|
return matched;
|
|
389
443
|
}
|
|
444
|
+
async _renderNginx(found, projectName, activePort) {
|
|
445
|
+
if (!found.nginx?.config_dir)
|
|
446
|
+
return null;
|
|
447
|
+
if (!activePort) {
|
|
448
|
+
return { code: 52, message: "Cannot render nginx: no PORT in merged config for project " + projectName, httpStatus: 500 };
|
|
449
|
+
}
|
|
450
|
+
const dir = found.nginx.config_dir;
|
|
451
|
+
try {
|
|
452
|
+
fs_1.default.mkdirSync(dir, { recursive: true });
|
|
453
|
+
}
|
|
454
|
+
catch (e) {
|
|
455
|
+
return { code: 50, message: "Cannot create nginx config_dir: " + String(e?.message ?? e), httpStatus: 500 };
|
|
456
|
+
}
|
|
457
|
+
const safeName = projectName.replace(/[^a-zA-Z0-9_]/g, "_");
|
|
458
|
+
const upstreamName = found.nginx.upstream_name || (safeName + "_backend");
|
|
459
|
+
const upstreamPath = path_1.default.resolve(dir, projectName + ".upstream.conf");
|
|
460
|
+
const serverPath = path_1.default.resolve(dir, projectName + ".server.conf");
|
|
461
|
+
const domain = found.nginx.domain || projectName;
|
|
462
|
+
if (!found.nginx.domain) {
|
|
463
|
+
(0, LogService_1.logWarn)("nginx.domain not set for project " + projectName +
|
|
464
|
+
" — seeding server.conf with server_name=" + projectName + " (edit or delete .server.conf to fix)");
|
|
465
|
+
}
|
|
466
|
+
const seedResult = NginxHelper_1.NginxHelper.seedServerConfig(serverPath, NginxHelper_1.DEFAULT_SERVER_TEMPLATE, {
|
|
467
|
+
DOMAIN: domain,
|
|
468
|
+
UPSTREAM: upstreamName,
|
|
469
|
+
});
|
|
470
|
+
if (seedResult.error)
|
|
471
|
+
return seedResult.error;
|
|
472
|
+
if (seedResult.seeded) {
|
|
473
|
+
(0, LogService_1.logWarn)("Seeded nginx server config for " + projectName + " at " + serverPath +
|
|
474
|
+
" — run `certbot --nginx -d " + domain + "` to add SSL");
|
|
475
|
+
}
|
|
476
|
+
const upstreamErr = NginxHelper_1.NginxHelper.writeUpstream(upstreamPath, upstreamName, activePort);
|
|
477
|
+
if (upstreamErr)
|
|
478
|
+
return upstreamErr;
|
|
479
|
+
return null;
|
|
480
|
+
}
|
|
390
481
|
async runPM2(found, projectState) {
|
|
391
482
|
let pm2attrs = projectState?.lastDeployed === "blue" ? found.bluegreen?.blue_pm2_attributes : found.bluegreen?.green_pm2_attributes;
|
|
392
483
|
const pm2Attrs = Array.isArray(pm2attrs) ? pm2attrs.filter(s => typeof s === "string" && s.length > 0) : found.pm2_attributes;
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
+
import { IError } from "../structures/Interfaces";
|
|
2
|
+
export declare const DEFAULT_SERVER_TEMPLATE = "# Seeded by deployer for _{{DOMAIN}}_. SAFE TO EDIT.\n# Deployer NEVER overwrites this file after the initial seed.\n# Add SSL by running: certbot --nginx -d _{{DOMAIN}}_\n\nserver {\n server_name _{{DOMAIN}}_;\n listen 80;\n\n location / {\n proxy_pass http://_{{UPSTREAM}}_;\n proxy_http_version 1.1;\n proxy_set_header Host $host;\n proxy_set_header X-Real-IP $remote_addr;\n proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n proxy_set_header X-Forwarded-Proto $scheme;\n proxy_read_timeout 60s;\n }\n}\n";
|
|
1
3
|
export declare class NginxHelper {
|
|
2
|
-
static
|
|
3
|
-
static
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
} | null;
|
|
4
|
+
static writeUpstream(filePath: string, upstreamName: string, port: string): IError | null;
|
|
5
|
+
static seedServerConfig(filePath: string, template: string, values: Record<string, string>): {
|
|
6
|
+
seeded: boolean;
|
|
7
|
+
error?: IError;
|
|
8
|
+
};
|
|
8
9
|
static runHook(cmd: string[] | undefined | null): Promise<string | null>;
|
|
9
10
|
}
|
|
@@ -3,50 +3,78 @@ 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.NginxHelper = void 0;
|
|
6
|
+
exports.NginxHelper = exports.DEFAULT_SERVER_TEMPLATE = void 0;
|
|
7
7
|
const fs_1 = __importDefault(require("fs"));
|
|
8
8
|
const path_1 = __importDefault(require("path"));
|
|
9
9
|
const child_process_1 = require("child_process");
|
|
10
10
|
const util_1 = require("util");
|
|
11
|
-
const crypto_1 = __importDefault(require("crypto"));
|
|
12
11
|
const LogService_1 = require("../LogService");
|
|
13
12
|
const execFileAsync = (0, util_1.promisify)(child_process_1.execFile);
|
|
13
|
+
exports.DEFAULT_SERVER_TEMPLATE = `# Seeded by deployer for _{{DOMAIN}}_. SAFE TO EDIT.
|
|
14
|
+
# Deployer NEVER overwrites this file after the initial seed.
|
|
15
|
+
# Add SSL by running: certbot --nginx -d _{{DOMAIN}}_
|
|
16
|
+
|
|
17
|
+
server {
|
|
18
|
+
server_name _{{DOMAIN}}_;
|
|
19
|
+
listen 80;
|
|
20
|
+
|
|
21
|
+
location / {
|
|
22
|
+
proxy_pass http://_{{UPSTREAM}}_;
|
|
23
|
+
proxy_http_version 1.1;
|
|
24
|
+
proxy_set_header Host $host;
|
|
25
|
+
proxy_set_header X-Real-IP $remote_addr;
|
|
26
|
+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
27
|
+
proxy_set_header X-Forwarded-Proto $scheme;
|
|
28
|
+
proxy_read_timeout 60s;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
`;
|
|
14
32
|
class NginxHelper {
|
|
15
|
-
static
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
33
|
+
static writeUpstream(filePath, upstreamName, port) {
|
|
34
|
+
const content = `# Auto-generated by deployer on each activate — do not edit.
|
|
35
|
+
upstream ${upstreamName} {
|
|
36
|
+
server 127.0.0.1:${port};
|
|
37
|
+
}
|
|
38
|
+
`;
|
|
39
|
+
try {
|
|
40
|
+
const tmp = filePath + ".tmp";
|
|
41
|
+
try {
|
|
42
|
+
fs_1.default.unlinkSync(tmp);
|
|
43
|
+
}
|
|
44
|
+
catch { }
|
|
45
|
+
fs_1.default.writeFileSync(tmp, content, "utf-8");
|
|
46
|
+
fs_1.default.renameSync(tmp, filePath);
|
|
47
|
+
return null;
|
|
27
48
|
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
const unique = [...new Set(leftover)];
|
|
31
|
-
(0, LogService_1.logWarn)("Unresolved template placeholders in nginx config " + filePath + ": " + unique.join(", "));
|
|
49
|
+
catch (e) {
|
|
50
|
+
return { code: 48, message: "Failed to write nginx upstream file: " + String(e?.message ?? e), httpStatus: 500 };
|
|
32
51
|
}
|
|
33
52
|
}
|
|
34
|
-
static
|
|
35
|
-
if (
|
|
36
|
-
return {
|
|
53
|
+
static seedServerConfig(filePath, template, values) {
|
|
54
|
+
if (fs_1.default.existsSync(filePath)) {
|
|
55
|
+
return { seeded: false };
|
|
37
56
|
}
|
|
38
57
|
try {
|
|
39
|
-
|
|
58
|
+
let content = template;
|
|
59
|
+
for (const [key, value] of Object.entries(values)) {
|
|
60
|
+
content = content.replaceAll(`_{{${key}}}_`, value);
|
|
61
|
+
}
|
|
62
|
+
const leftover = content.match(/_\{\{[A-Za-z0-9_]+\}\}_/g);
|
|
63
|
+
if (leftover && leftover.length > 0) {
|
|
64
|
+
const unique = [...new Set(leftover)];
|
|
65
|
+
(0, LogService_1.logWarn)("Unresolved placeholders in seeded nginx server config " + filePath + ": " + unique.join(", "));
|
|
66
|
+
}
|
|
67
|
+
const tmp = filePath + ".tmp";
|
|
40
68
|
try {
|
|
41
69
|
fs_1.default.unlinkSync(tmp);
|
|
42
70
|
}
|
|
43
71
|
catch { }
|
|
44
|
-
fs_1.default.
|
|
45
|
-
fs_1.default.renameSync(tmp,
|
|
46
|
-
return
|
|
72
|
+
fs_1.default.writeFileSync(tmp, content, "utf-8");
|
|
73
|
+
fs_1.default.renameSync(tmp, filePath);
|
|
74
|
+
return { seeded: true };
|
|
47
75
|
}
|
|
48
76
|
catch (e) {
|
|
49
|
-
return { code: 49, message: "nginx
|
|
77
|
+
return { seeded: false, error: { code: 49, message: "Failed to seed nginx server config: " + String(e?.message ?? e), httpStatus: 500 } };
|
|
50
78
|
}
|
|
51
79
|
}
|
|
52
80
|
static async runHook(cmd) {
|
|
@@ -424,15 +424,22 @@ class Http {
|
|
|
424
424
|
const hasBody = opts.body !== undefined && opts.body !== null;
|
|
425
425
|
const allowBody = opts.allowBody ?? (opts.method !== "GET" && opts.method !== "DELETE");
|
|
426
426
|
if (hasBody && allowBody) {
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
headers["content-type"]
|
|
430
|
-
|
|
431
|
-
if (ct2.includes("application/json") && typeof opts.body !== "string" && !(opts.body instanceof Buffer)) {
|
|
432
|
-
body = JSON.stringify(opts.body);
|
|
427
|
+
if (typeof FormData !== "undefined" && opts.body instanceof FormData) {
|
|
428
|
+
body = opts.body;
|
|
429
|
+
delete headers["content-type"];
|
|
430
|
+
delete headers["Content-Type"];
|
|
433
431
|
}
|
|
434
432
|
else {
|
|
435
|
-
|
|
433
|
+
const ct = (headers["content-type"] || headers["Content-Type"] || "").toLowerCase();
|
|
434
|
+
if (!ct)
|
|
435
|
+
headers["content-type"] = "application/json";
|
|
436
|
+
const ct2 = (headers["content-type"] || headers["Content-Type"] || "").toLowerCase();
|
|
437
|
+
if (ct2.includes("application/json") && typeof opts.body !== "string" && !(opts.body instanceof Buffer)) {
|
|
438
|
+
body = JSON.stringify(opts.body);
|
|
439
|
+
}
|
|
440
|
+
else {
|
|
441
|
+
body = opts.body;
|
|
442
|
+
}
|
|
436
443
|
}
|
|
437
444
|
}
|
|
438
445
|
const responseType = opts.responseType ?? "json";
|
package/dist/index.d.ts
CHANGED
|
@@ -17,4 +17,5 @@ import { Http } from "./apiServer/http/Http";
|
|
|
17
17
|
import { MicroserviceHost } from "./apiServer/external/MicroserviceHost";
|
|
18
18
|
import { MicroserviceClient } from "./apiServer/external/MicroserviceClient";
|
|
19
19
|
import { Deploy } from "./apiServer/deployment/Deploy";
|
|
20
|
-
|
|
20
|
+
import { Activate } from "./apiServer/deployment/Activate";
|
|
21
|
+
export { MicroserviceHost, MicroserviceClient, Http, ZipUtils, UID, YYYYMMDDHH, JSONStableStringify, APIService, Initializer, LocalRequest, ValidationModel, MysqlService, TimeframeService, Validator, LogService, DataProvider, ErrorUtils, ExternalService, DBService, Deploy, Activate, S_MONITOR_REGISTRATE_ACTION };
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.S_MONITOR_REGISTRATE_ACTION = exports.Deploy = exports.DBService = exports.ExternalService = exports.ErrorUtils = exports.DataProvider = exports.LogService = exports.Validator = exports.TimeframeService = exports.MysqlService = exports.LocalRequest = exports.Initializer = exports.APIService = exports.JSONStableStringify = exports.YYYYMMDDHH = exports.UID = exports.ZipUtils = exports.Http = exports.MicroserviceClient = exports.MicroserviceHost = void 0;
|
|
3
|
+
exports.S_MONITOR_REGISTRATE_ACTION = exports.Activate = exports.Deploy = exports.DBService = exports.ExternalService = exports.ErrorUtils = exports.DataProvider = exports.LogService = exports.Validator = exports.TimeframeService = exports.MysqlService = exports.LocalRequest = exports.Initializer = exports.APIService = exports.JSONStableStringify = exports.YYYYMMDDHH = exports.UID = exports.ZipUtils = exports.Http = exports.MicroserviceClient = exports.MicroserviceHost = void 0;
|
|
4
4
|
const APIService_1 = require("./apiServer/APIService");
|
|
5
5
|
Object.defineProperty(exports, "APIService", { enumerable: true, get: function () { return APIService_1.APIService; } });
|
|
6
6
|
Object.defineProperty(exports, "Initializer", { enumerable: true, get: function () { return APIService_1.Initializer; } });
|
|
@@ -40,3 +40,5 @@ const MicroserviceClient_1 = require("./apiServer/external/MicroserviceClient");
|
|
|
40
40
|
Object.defineProperty(exports, "MicroserviceClient", { enumerable: true, get: function () { return MicroserviceClient_1.MicroserviceClient; } });
|
|
41
41
|
const Deploy_1 = require("./apiServer/deployment/Deploy");
|
|
42
42
|
Object.defineProperty(exports, "Deploy", { enumerable: true, get: function () { return Deploy_1.Deploy; } });
|
|
43
|
+
const Activate_1 = require("./apiServer/deployment/Activate");
|
|
44
|
+
Object.defineProperty(exports, "Activate", { enumerable: true, get: function () { return Activate_1.Activate; } });
|