@oondemand/oon-core-back 0.7.1 → 0.7.2
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/bin/build-delivery-image.js +3 -1
- package/bin/check-delivery-source.js +10 -0
- package/package.json +1 -1
- package/src/delivery/buildImage.js +39 -11
- package/src/delivery/launchRuntime.js +13 -0
- package/src/delivery/supervisor.js +52 -0
- package/src/delivery/testSource.js +64 -0
- package/src/delivery/validateImage.js +124 -0
|
@@ -8,4 +8,6 @@ const workspace = path.resolve(flag("--workspace") || process.cwd());
|
|
|
8
8
|
const tag = flag("--tag");
|
|
9
9
|
const output = flag("--output");
|
|
10
10
|
if (!tag || !output) { console.error("--tag e --output são obrigatórios"); process.exit(2); }
|
|
11
|
-
buildImage({ workspace, tag, output: path.resolve(output) })
|
|
11
|
+
buildImage({ workspace, tag, output: path.resolve(output), diagnosticOutput: flag("--diagnostic-output") })
|
|
12
|
+
.catch((error) => { console.error(error.code || "IMAGE_BUILD_FAILED"); process.exitCode = 1; });
|
|
13
|
+
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
const path = require("node:path");
|
|
4
|
+
const { testSource } = require("../src/delivery/testSource");
|
|
5
|
+
function flag(name) { const i = process.argv.indexOf(name); return i < 0 ? undefined : process.argv[i + 1]; }
|
|
6
|
+
const output = flag("--diagnostic-output");
|
|
7
|
+
if (!output) throw new Error("--diagnostic-output é obrigatório");
|
|
8
|
+
const result = testSource({ workspace: path.resolve(flag("--workspace") || process.cwd()), output });
|
|
9
|
+
console.log(JSON.stringify({ status: result.status, suites: result.suites }));
|
|
10
|
+
if (result.status === "failed") process.exitCode = 1;
|
package/package.json
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
const fs = require("node:fs");
|
|
4
4
|
const path = require("node:path");
|
|
5
5
|
const { run } = require("./command");
|
|
6
|
+
const { validateImage } = require("./validateImage");
|
|
6
7
|
|
|
7
8
|
const DOCKERFILE = `# syntax=docker/dockerfile:1.7
|
|
8
9
|
FROM node:20-alpine AS frontend
|
|
@@ -28,19 +29,32 @@ ENV NODE_ENV=production
|
|
|
28
29
|
COPY --from=backend /app/backend /app/backend
|
|
29
30
|
COPY .oon-delivery/app-manifest/ /app/
|
|
30
31
|
COPY --from=frontend /src/frontend/dist /usr/share/nginx/html
|
|
31
|
-
COPY .oon-delivery/nginx.conf /etc/nginx/
|
|
32
|
+
COPY .oon-delivery/nginx.conf /etc/nginx/oon.conf
|
|
33
|
+
COPY .oon-delivery/supervisor.js /supervisor.js
|
|
34
|
+
COPY .oon-delivery/launchRuntime.js .oon-delivery/validateImage.js /oon-delivery/
|
|
32
35
|
COPY .oon-delivery/entrypoint.sh /entrypoint.sh
|
|
33
|
-
RUN chmod +x /entrypoint.sh && mkdir -p /app/backend/uploads /run/nginx
|
|
36
|
+
RUN chmod +x /entrypoint.sh && mkdir -p /app/backend/uploads /run/nginx && chown -R node:node /app/backend/uploads
|
|
34
37
|
EXPOSE 8080
|
|
35
38
|
HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=3 CMD curl -fsS http://127.0.0.1:8080/health/live || exit 1
|
|
36
39
|
ENTRYPOINT ["/entrypoint.sh"]
|
|
37
40
|
`;
|
|
38
41
|
|
|
39
|
-
const NGINX = `
|
|
42
|
+
const NGINX = `pid /tmp/nginx.pid;
|
|
43
|
+
error_log /dev/stderr;
|
|
44
|
+
events {}
|
|
45
|
+
http {
|
|
46
|
+
include /etc/nginx/mime.types;
|
|
47
|
+
access_log /dev/stdout;
|
|
48
|
+
client_body_temp_path /tmp/client_temp;
|
|
49
|
+
proxy_temp_path /tmp/proxy_temp;
|
|
50
|
+
fastcgi_temp_path /tmp/fastcgi_temp;
|
|
51
|
+
uwsgi_temp_path /tmp/uwsgi_temp;
|
|
52
|
+
scgi_temp_path /tmp/scgi_temp;
|
|
53
|
+
server {
|
|
40
54
|
listen 8080;
|
|
41
55
|
server_name _;
|
|
42
56
|
root /usr/share/nginx/html;
|
|
43
|
-
location = /health/live { access_log off;
|
|
57
|
+
location = /health/live { access_log off; proxy_pass http://127.0.0.1:4000/; }
|
|
44
58
|
location /api/ {
|
|
45
59
|
proxy_pass http://127.0.0.1:4000/;
|
|
46
60
|
proxy_http_version 1.1;
|
|
@@ -50,16 +64,12 @@ const NGINX = `server {
|
|
|
50
64
|
}
|
|
51
65
|
location / { try_files $uri $uri/ /index.html; }
|
|
52
66
|
}
|
|
67
|
+
}
|
|
53
68
|
`;
|
|
54
69
|
|
|
55
70
|
const ENTRYPOINT = `#!/bin/sh
|
|
56
71
|
set -eu
|
|
57
|
-
|
|
58
|
-
export PORT=4000
|
|
59
|
-
./node_modules/.bin/oonCore-back start &
|
|
60
|
-
backend_pid=$!
|
|
61
|
-
trap 'kill $backend_pid 2>/dev/null || true' TERM INT EXIT
|
|
62
|
-
exec nginx -g 'daemon off;'
|
|
72
|
+
exec node /supervisor.js
|
|
63
73
|
`;
|
|
64
74
|
|
|
65
75
|
function prepareAppManifest(workspace, deliveryDirectory) {
|
|
@@ -96,19 +106,36 @@ function prepareAppManifest(workspace, deliveryDirectory) {
|
|
|
96
106
|
|
|
97
107
|
function prepareDeliveryFiles(workspace) {
|
|
98
108
|
const target = path.join(workspace, ".oon-delivery");
|
|
109
|
+
if (fs.existsSync(target) && fs.lstatSync(target).isSymbolicLink()) throw new Error("Diretório de delivery não pode ser symlink.");
|
|
110
|
+
fs.rmSync(target, { recursive: true, force: true });
|
|
99
111
|
fs.mkdirSync(target, { recursive: true });
|
|
100
112
|
prepareAppManifest(workspace, target);
|
|
113
|
+
for (const file of ["supervisor.js", "launchRuntime.js", "validateImage.js"]) fs.copyFileSync(path.join(__dirname, file), path.join(target, file));
|
|
101
114
|
fs.writeFileSync(path.join(target, "Dockerfile"), DOCKERFILE);
|
|
102
115
|
fs.writeFileSync(path.join(target, "nginx.conf"), NGINX);
|
|
103
116
|
fs.writeFileSync(path.join(target, "entrypoint.sh"), ENTRYPOINT, {
|
|
104
117
|
mode: 0o755,
|
|
105
118
|
});
|
|
119
|
+
// Dockerfile-specific ignore rules win over the root ignore file. Preserve
|
|
120
|
+
// user rules, then append mandatory exclusions so local installs and secrets
|
|
121
|
+
// cannot overwrite production dependencies or leak into the final image.
|
|
122
|
+
const userIgnore = path.join(workspace, ".dockerignore");
|
|
123
|
+
const ignore = fs.existsSync(userIgnore) ? fs.readFileSync(userIgnore, "utf8") : "";
|
|
124
|
+
fs.writeFileSync(path.join(target, "Dockerfile.dockerignore"), ignore + "\n**/node_modules\n**/.git\n**/.env\n**/.env.*\n**/*.pem\n**/*.key\n**/.npmrc\n!frontend/.env.production\n");
|
|
106
125
|
return path.join(target, "Dockerfile");
|
|
107
126
|
}
|
|
108
127
|
|
|
109
|
-
function buildImage({ workspace, tag, output }) {
|
|
128
|
+
async function buildImage({ workspace, tag, output, diagnosticOutput }) {
|
|
129
|
+
fs.rmSync(output, { force: true });
|
|
110
130
|
const dockerfile = prepareDeliveryFiles(workspace);
|
|
111
131
|
run("docker", ["build", "-f", dockerfile, "-t", tag, workspace]);
|
|
132
|
+
const validation = await validateImage({ tag });
|
|
133
|
+
if (diagnosticOutput) fs.writeFileSync(diagnosticOutput, JSON.stringify(validation, null, 2));
|
|
134
|
+
if (validation.status !== "passed") {
|
|
135
|
+
const error = new Error(validation.cause);
|
|
136
|
+
error.code = validation.code;
|
|
137
|
+
throw error;
|
|
138
|
+
}
|
|
112
139
|
run("docker", ["save", "-o", output, tag]);
|
|
113
140
|
return { dockerfile, output, tag };
|
|
114
141
|
}
|
|
@@ -121,3 +148,4 @@ module.exports = {
|
|
|
121
148
|
prepareDeliveryFiles,
|
|
122
149
|
buildImage,
|
|
123
150
|
};
|
|
151
|
+
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
const { createRequire } = require("node:module");
|
|
3
|
+
const { classify } = require("./validateImage");
|
|
4
|
+
const runtimeRequire = createRequire("/app/backend/package.json");
|
|
5
|
+
// Invoke the public API of the SDK INSIDE the image; do not replace the app's
|
|
6
|
+
// dependency version. Include the error class so SyntaxError is not lost by an
|
|
7
|
+
// older CLI that prints only error.message. Never print the raw error/stack.
|
|
8
|
+
Promise.resolve().then(() => runtimeRequire("@oondemand/oon-core-back").start({ cwd: "/app/backend" }))
|
|
9
|
+
.catch((error) => {
|
|
10
|
+
const diagnostic = classify(`${error?.name || "Error"}: ${error?.message || ""}`);
|
|
11
|
+
console.error(`OON_STARTUP_FAILURE ${JSON.stringify(diagnostic)}`);
|
|
12
|
+
process.exit(1);
|
|
13
|
+
});
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// PID 1 owns both process groups. An unexpected exit (including status 0)
|
|
4
|
+
// terminates the container; a live proxy cannot hide a dead backend.
|
|
5
|
+
const { spawn } = require("node:child_process");
|
|
6
|
+
|
|
7
|
+
function supervise({ backend, proxy, graceMs = 5000 } = {}) {
|
|
8
|
+
const children = [];
|
|
9
|
+
let stopping = false;
|
|
10
|
+
let exitCode = 1;
|
|
11
|
+
let timer;
|
|
12
|
+
const signalGroup = (child, signal) => {
|
|
13
|
+
if (!child.pid) return;
|
|
14
|
+
try { process.kill(-child.pid, signal); } catch (error) {
|
|
15
|
+
if (error.code !== "ESRCH") throw error;
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
const finish = () => {
|
|
19
|
+
if (stopping && children.every((child) => child.failed || child.exitCode !== null || child.signalCode !== null)) {
|
|
20
|
+
clearTimeout(timer);
|
|
21
|
+
process.exitCode = exitCode;
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
const stop = (code) => {
|
|
25
|
+
if (stopping) return;
|
|
26
|
+
stopping = true;
|
|
27
|
+
exitCode = code;
|
|
28
|
+
for (const child of children) signalGroup(child, "SIGTERM");
|
|
29
|
+
timer = setTimeout(() => {
|
|
30
|
+
for (const child of children) signalGroup(child, "SIGKILL");
|
|
31
|
+
}, graceMs);
|
|
32
|
+
finish();
|
|
33
|
+
};
|
|
34
|
+
for (const spec of [backend, proxy]) {
|
|
35
|
+
const child = spawn(spec.command, spec.args || [], {
|
|
36
|
+
cwd: spec.cwd, env: { ...process.env, PORT: "4000" },
|
|
37
|
+
stdio: "inherit", detached: true,
|
|
38
|
+
});
|
|
39
|
+
children.push(child);
|
|
40
|
+
child.once("error", () => { child.failed = true; stop(1); finish(); });
|
|
41
|
+
child.once("exit", () => { stop(1); finish(); });
|
|
42
|
+
}
|
|
43
|
+
process.once("SIGTERM", () => stop(0));
|
|
44
|
+
process.once("SIGINT", () => stop(0));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (require.main === module) supervise({
|
|
48
|
+
backend: { command: process.execPath, args: ["/oon-delivery/launchRuntime.js"], cwd: "/app/backend" },
|
|
49
|
+
proxy: { command: "nginx", args: ["-c", "/etc/nginx/oon.conf", "-g", "daemon off;"] },
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
module.exports = { supervise };
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
const fs = require("node:fs");
|
|
3
|
+
const path = require("node:path");
|
|
4
|
+
const { spawnSync } = require("node:child_process");
|
|
5
|
+
const { randomUUID } = require("node:crypto");
|
|
6
|
+
const { prepareDeliveryFiles } = require("./buildImage");
|
|
7
|
+
|
|
8
|
+
function testPlan(workspace) {
|
|
9
|
+
const scopes = [".", "backend", "frontend"].map((scope) => {
|
|
10
|
+
const file = path.join(workspace, scope, "package.json");
|
|
11
|
+
const declared = fs.existsSync(file) && typeof JSON.parse(fs.readFileSync(file, "utf8")).scripts?.test === "string";
|
|
12
|
+
return { scope, status: declared ? "declared" : "not_declared" };
|
|
13
|
+
});
|
|
14
|
+
// A root suite is the orchestration entrypoint. Components are run separately
|
|
15
|
+
// only when no root test is declared. This avoids executing a suite twice.
|
|
16
|
+
if (scopes[0].status === "declared") {
|
|
17
|
+
for (const item of scopes.slice(1)) if (item.status === "declared") item.status = "root_orchestrated";
|
|
18
|
+
}
|
|
19
|
+
return scopes;
|
|
20
|
+
}
|
|
21
|
+
function testSource({ workspace, output }) {
|
|
22
|
+
const suites = testPlan(workspace);
|
|
23
|
+
const result = { schemaVersion: 1, stage: "source_tests", deployStarted: false, suites };
|
|
24
|
+
const selected = suites.filter((s) => s.status === "declared");
|
|
25
|
+
if (!selected.length) {
|
|
26
|
+
result.status = "not_declared";
|
|
27
|
+
fs.writeFileSync(output, JSON.stringify(result));
|
|
28
|
+
return result;
|
|
29
|
+
}
|
|
30
|
+
prepareDeliveryFiles(workspace);
|
|
31
|
+
const target = path.join(workspace, ".oon-delivery");
|
|
32
|
+
const dockerfile = path.join(target, "Dockerfile.test");
|
|
33
|
+
fs.copyFileSync(path.join(target, "Dockerfile.dockerignore"), `${dockerfile}.dockerignore`);
|
|
34
|
+
fs.writeFileSync(dockerfile, `FROM node:20-alpine\nWORKDIR /work\nCOPY . .\nRUN for scope in . backend frontend; do if [ -f "$scope/package.json" ]; then (cd "$scope" && if [ -f package-lock.json ]; then npm ci --ignore-scripts; else npm install --ignore-scripts; fi); fi; done\nRUN chown -R node:node /work\nUSER node\n`);
|
|
35
|
+
const name = `oon-tests-${randomUUID()}`;
|
|
36
|
+
const invoke = (args) => spawnSync("docker", args, { encoding: "utf8", timeout: 600000, maxBuffer: 1024 * 1024 });
|
|
37
|
+
try {
|
|
38
|
+
const build = invoke(["build", "-f", dockerfile, "-t", name, workspace]);
|
|
39
|
+
if (build.error || build.status !== 0) {
|
|
40
|
+
result.status = "failed"; result.category = "unknown"; result.code = "SOURCE_TEST_SETUP_FAILED";
|
|
41
|
+
result.cause = "Não foi possível preparar as dependências dos testes.";
|
|
42
|
+
result.guidance = "Confira lockfiles, dependências e disponibilidade do registry. Scripts de instalação não são executados nesta etapa.";
|
|
43
|
+
} else {
|
|
44
|
+
for (const suite of selected) {
|
|
45
|
+
const run = invoke(["run", "--rm", "--name", name, "--network", "none", "--cap-drop=ALL", "--security-opt=no-new-privileges", "--memory=1g", "--cpus=2", "--pids-limit=256", "--workdir", path.posix.join("/work", suite.scope), name, "npm", "test"]);
|
|
46
|
+
suite.status = run.status === 0 && !run.error ? "passed" : "failed";
|
|
47
|
+
if (suite.status === "failed") break;
|
|
48
|
+
}
|
|
49
|
+
for (const suite of selected) if (suite.status === "declared") suite.status = "not_run";
|
|
50
|
+
result.status = suites.some((s) => s.status === "failed") ? "failed" : "passed";
|
|
51
|
+
if (result.status === "failed") {
|
|
52
|
+
result.category = "unknown"; result.code = "SOURCE_TEST_FAILED";
|
|
53
|
+
result.cause = "A suíte declarada não passou no ambiente isolado.";
|
|
54
|
+
result.guidance = "Execute os testes declarados e confira dependências; serviços externos não são acessíveis durante os testes.";
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
} finally {
|
|
58
|
+
invoke(["rm", "-f", "-v", name]);
|
|
59
|
+
invoke(["image", "rm", "-f", name]);
|
|
60
|
+
fs.writeFileSync(output, JSON.stringify(result, null, 2));
|
|
61
|
+
}
|
|
62
|
+
return result;
|
|
63
|
+
}
|
|
64
|
+
module.exports = { testSource, testPlan };
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { spawnSync } = require("node:child_process");
|
|
4
|
+
const { randomUUID } = require("node:crypto");
|
|
5
|
+
|
|
6
|
+
// No arbitrary logs cross the build/privileged-job boundary. Only a bounded
|
|
7
|
+
// allowlist of paths and stable codes is returned; messages are platform-owned.
|
|
8
|
+
function safePath(value) {
|
|
9
|
+
const text = String(value || "").replace(/^\/app\//, "");
|
|
10
|
+
return text.length <= 240 && /^(?:[.@a-zA-Z0-9_-]+\/)*[.@a-zA-Z0-9_-]+$/.test(text)
|
|
11
|
+
&& !/(?:secret|token|password|credential|\.env)/i.test(text) ? text : undefined;
|
|
12
|
+
}
|
|
13
|
+
function classify(log = "") {
|
|
14
|
+
const recorded = log.match(/OON_STARTUP_FAILURE (\{[^\r\n]+\})/);
|
|
15
|
+
if (recorded) {
|
|
16
|
+
try {
|
|
17
|
+
const input = JSON.parse(recorded[1]);
|
|
18
|
+
const known = ["IMAGE_MODULE_NOT_FOUND", "IMAGE_SYNTAX_ERROR", "IMAGE_DEPENDENCY_UNAVAILABLE", "IMAGE_STARTUP_FAILED"];
|
|
19
|
+
if (known.includes(input.code)) {
|
|
20
|
+
// Reconstruct through the same bounded classifier; no arbitrary fields.
|
|
21
|
+
const representative = input.code === "IMAGE_MODULE_NOT_FOUND"
|
|
22
|
+
? `Cannot find module '${safePath(input.path) || "unavailable"}'\nRequire stack:\n- ${safePath(input.importer) || "unavailable"}`
|
|
23
|
+
: input.code === "IMAGE_SYNTAX_ERROR" ? "SyntaxError:" : input.code === "IMAGE_DEPENDENCY_UNAVAILABLE" ? "ECONNREFUSED" : "";
|
|
24
|
+
return classify(representative);
|
|
25
|
+
}
|
|
26
|
+
} catch { /* Treat malformed logs as indeterminate. */ }
|
|
27
|
+
}
|
|
28
|
+
const missing = log.match(/Cannot find module ['"]([^'"\r\n]+)['"]/);
|
|
29
|
+
if (missing) return {
|
|
30
|
+
category: "application", code: "IMAGE_MODULE_NOT_FOUND",
|
|
31
|
+
cause: "Módulo ou asset necessário não está disponível na imagem de produção.",
|
|
32
|
+
path: safePath(missing[1]),
|
|
33
|
+
importer: safePath(log.match(/Require stack:\s*\n- ([^\r\n]+)/)?.[1]),
|
|
34
|
+
guidance: "Inclua assets de runtime em backend e declare dependências em dependencies. Confira o importador e o contrato de empacotamento.",
|
|
35
|
+
};
|
|
36
|
+
if (/SyntaxError:/.test(log)) return {
|
|
37
|
+
category: "application", code: "IMAGE_SYNTAX_ERROR",
|
|
38
|
+
cause: "Um módulo da imagem contém sintaxe inválida.",
|
|
39
|
+
guidance: "Revise a sintaxe dos módulos e a compatibilidade com Node 20.",
|
|
40
|
+
};
|
|
41
|
+
if (/Mongo(?:ose)?ServerSelectionError|ECONNREFUSED|ENOTFOUND|ETIMEDOUT/.test(log)) return {
|
|
42
|
+
category: "configuration", code: "IMAGE_DEPENDENCY_UNAVAILABLE",
|
|
43
|
+
cause: "A aplicação tentou acessar uma dependência indisponível na validação isolada.",
|
|
44
|
+
guidance: "Confira a configuração de inicialização. Serviços reais não são acessíveis durante esta validação.",
|
|
45
|
+
};
|
|
46
|
+
return {
|
|
47
|
+
category: "unknown", code: "IMAGE_STARTUP_FAILED",
|
|
48
|
+
cause: "A imagem não concluiu a inicialização; a causa não foi determinada.",
|
|
49
|
+
guidance: "Reproduza a inicialização em ambiente isolado e confira a configuração e os módulos carregados.",
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
function docker(args, { timeout = 30000, allowFailure = false } = {}) {
|
|
53
|
+
const r = spawnSync("docker", args, { encoding: "utf8", timeout, maxBuffer: 1024 * 1024 });
|
|
54
|
+
if (!allowFailure && (r.error || r.status !== 0)) {
|
|
55
|
+
const error = new Error("O executor de validação não concluiu uma operação Docker.");
|
|
56
|
+
error.code = "IMAGE_VALIDATION_INFRASTRUCTURE";
|
|
57
|
+
throw error;
|
|
58
|
+
}
|
|
59
|
+
return r;
|
|
60
|
+
}
|
|
61
|
+
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
62
|
+
|
|
63
|
+
async function validateImage({ tag, timeoutMs = 90000, execute = docker, sleep = delay }) {
|
|
64
|
+
const id = `oon-validation-${randomUUID()}`;
|
|
65
|
+
const database = `${id}-db`, app = `${id}-app`;
|
|
66
|
+
const base = { schemaVersion: 1, stage: "image_validation", deployStarted: false };
|
|
67
|
+
let imageId;
|
|
68
|
+
let infrastructureStep = "image_inspect";
|
|
69
|
+
try {
|
|
70
|
+
imageId = execute(["image", "inspect", "--format", "{{.Id}}", tag]).stdout.trim();
|
|
71
|
+
if (!/^sha256:[a-f0-9]{64}$/.test(imageId)) throw new Error("Invalid image identity");
|
|
72
|
+
// Pull BEFORE attaching services to an internal-only network. No host mounts,
|
|
73
|
+
// published ports, host networking, Docker socket, or real credentials.
|
|
74
|
+
infrastructureStep = "database_image";
|
|
75
|
+
execute(["pull", "mongo:7"], { timeout: 180000 });
|
|
76
|
+
infrastructureStep = "database_startup";
|
|
77
|
+
execute(["run", "-d", "--name", database, "--network", "none", "--user", "999:999", "--cap-drop=ALL",
|
|
78
|
+
"--security-opt=no-new-privileges", "--memory=1g", "--cpus=1", "--pids-limit=128",
|
|
79
|
+
"--read-only", "--tmpfs", "/data/db:rw,uid=999,gid=999,size=536870912",
|
|
80
|
+
"--tmpfs", "/data/configdb:rw,uid=999,gid=999,size=16777216",
|
|
81
|
+
"--tmpfs", "/tmp:rw,size=67108864", "mongo:7", "mongod", "--bind_ip_all", "--wiredTigerCacheSizeGB", "0.25"]);
|
|
82
|
+
let databaseReady = false;
|
|
83
|
+
for (let attempt = 0; attempt < 30; attempt++) {
|
|
84
|
+
const ping = execute(["exec", database, "mongosh", "--quiet", "--eval", "quit(db.adminCommand({ping:1}).ok ? 0 : 1)"], { allowFailure: true, timeout: 3000 });
|
|
85
|
+
if (ping.status === 0) { databaseReady = true; break; }
|
|
86
|
+
await sleep(1000);
|
|
87
|
+
}
|
|
88
|
+
if (!databaseReady) throw new Error("Validation database unavailable");
|
|
89
|
+
infrastructureStep = "application_startup";
|
|
90
|
+
execute(["run", "-d", "--name", app, "--network", `container:${database}`, "--user", "1000:1000",
|
|
91
|
+
"--cap-drop=ALL", "--security-opt=no-new-privileges", "--read-only",
|
|
92
|
+
"--memory=512m", "--cpus=1", "--pids-limit=128",
|
|
93
|
+
"--tmpfs", "/tmp:rw,uid=1000,gid=1000,size=67108864",
|
|
94
|
+
"--tmpfs", "/app/backend/uploads:rw,uid=1000,gid=1000,size=16777216",
|
|
95
|
+
"--env", "MONGO_URI=mongodb://127.0.0.1:27017/validation",
|
|
96
|
+
"--env", "NODE_ENV=production", "--env", "APP_ENVIRONMENT=desenvolvimento",
|
|
97
|
+
"--env", "INSTANCE_CREDENTIAL_ENCRYPTION_KEY=validation-only-not-a-real-credential",
|
|
98
|
+
"--env", "CENTRAL_ATIVACAO_URL=http://unavailable.invalid",
|
|
99
|
+
"--env", "CENTRAL_ATIVACAO_API_URL=http://unavailable.invalid", imageId]);
|
|
100
|
+
const deadline = Date.now() + timeoutMs;
|
|
101
|
+
let consecutive = 0;
|
|
102
|
+
while (Date.now() < deadline) {
|
|
103
|
+
const running = execute(["inspect", "--format", "{{.State.Running}}", app]).stdout.trim();
|
|
104
|
+
if (running !== "true") {
|
|
105
|
+
const logs = execute(["logs", "--tail", "80", app], { allowFailure: true });
|
|
106
|
+
return { ...base, imageId, status: "failed", ...classify(`${logs.stdout || ""}\n${logs.stderr || ""}`) };
|
|
107
|
+
}
|
|
108
|
+
const health = execute(["exec", app, "curl", "-fsS", "--max-time", "2", "http://127.0.0.1:8080/api/health/ready"], { allowFailure: true, timeout: 4000 });
|
|
109
|
+
if (health.status === 0) consecutive++; else consecutive = 0;
|
|
110
|
+
if (consecutive >= 3) return { ...base, imageId, status: "passed" };
|
|
111
|
+
await sleep(1000);
|
|
112
|
+
}
|
|
113
|
+
return { ...base, imageId, status: "failed", category: "unknown", code: "IMAGE_VALIDATION_TIMEOUT",
|
|
114
|
+
cause: "A inicialização não terminou no prazo de validação.",
|
|
115
|
+
guidance: "Investigue inicialização e dependências. Timeout isolado não comprova erro de código." };
|
|
116
|
+
} catch {
|
|
117
|
+
return { ...base, imageId, status: "failed", category: "infrastructure", code: "IMAGE_VALIDATION_INFRASTRUCTURE", infrastructureStep,
|
|
118
|
+
cause: "O ambiente de validação não conseguiu executar a prova da imagem.",
|
|
119
|
+
guidance: "Verifique Docker, recursos e disponibilidade da imagem do serviço temporário; tente novamente." };
|
|
120
|
+
} finally {
|
|
121
|
+
execute(["rm", "-f", "-v", app, database], { allowFailure: true });
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
module.exports = { validateImage, classify, safePath };
|