@naviyra/cloud-panel 0.0.0-stage → 0.0.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/.env.example +40 -0
- package/README.md +145 -2
- package/apps/agent/package.json +13 -0
- package/apps/agent/src/cloudflare.mjs +733 -0
- package/apps/agent/src/native-worker.mjs +52 -0
- package/apps/agent/src/native.mjs +457 -0
- package/apps/agent/src/runtime.mjs +760 -0
- package/apps/agent/src/server.mjs +195 -0
- package/apps/agent/src/static-server.mjs +53 -0
- package/apps/panel/AGENTS.md +9 -0
- package/apps/panel/CLAUDE.md +1 -0
- package/apps/panel/app/api/[...path]/route.js +445 -0
- package/apps/panel/app/globals.css +2057 -0
- package/apps/panel/app/icon.png +0 -0
- package/apps/panel/app/layout.jsx +18 -0
- package/apps/panel/app/login/page.jsx +108 -0
- package/apps/panel/app/page.jsx +33 -0
- package/apps/panel/app/ui/cloudflare.jsx +926 -0
- package/apps/panel/app/ui/dashboard.jsx +1487 -0
- package/apps/panel/app/ui/databases.jsx +293 -0
- package/apps/panel/app/ui/skeleton.jsx +196 -0
- package/apps/panel/app/ui/theme.jsx +35 -0
- package/apps/panel/lib/agent.mjs +83 -0
- package/apps/panel/lib/auth.mjs +48 -0
- package/apps/panel/lib/database-runtime.mjs +243 -0
- package/apps/panel/lib/databases.mjs +292 -0
- package/apps/panel/lib/db.mjs +6 -0
- package/apps/panel/next.config.mjs +8 -0
- package/apps/panel/package.json +22 -0
- package/apps/panel/public/naviyra-logo.png +0 -0
- package/bin/naviyra.mjs +215 -0
- package/db/001_init.sql +28 -0
- package/db/002_native_runtimes.sql +3 -0
- package/db/003_databases.sql +8 -0
- package/db/004_database_engines.sql +4 -0
- package/deploy/naviyra-agent.service +17 -0
- package/docker-compose.yml +18 -0
- package/docs/managed-runtime.md +52 -0
- package/docs/native-hosts.md +48 -0
- package/package.json +61 -4
- package/packages/runtime/artifacts.json +32 -0
- package/packages/runtime/index.cjs +526 -0
- package/packages/runtime/index.mjs +10 -0
- package/packages/runtime/package.json +6 -0
- package/packages/shared/index.mjs +85 -0
- package/packages/shared/package.json +9 -0
- package/scripts/migrate.mjs +18 -0
- package/scripts/panel-control.mjs +195 -0
- package/scripts/postgres-local.mjs +97 -0
- package/scripts/runtime-download-manifest.mjs +30 -0
- package/scripts/runtime.mjs +20 -0
- package/scripts/seed.mjs +35 -0
- package/scripts/service-supervisor.mjs +164 -0
- package/scripts/setup-local.mjs +215 -0
- package/scripts/start-naviyra.mjs +303 -0
- package/scripts/windows-boot.mjs +259 -0
|
@@ -0,0 +1,526 @@
|
|
|
1
|
+
const { spawn } = require("node:child_process");
|
|
2
|
+
const { createHash, randomUUID } = require("node:crypto");
|
|
3
|
+
const {
|
|
4
|
+
createReadStream,
|
|
5
|
+
createWriteStream,
|
|
6
|
+
readFileSync,
|
|
7
|
+
} = require("node:fs");
|
|
8
|
+
const {
|
|
9
|
+
access,
|
|
10
|
+
mkdir,
|
|
11
|
+
readFile,
|
|
12
|
+
rename,
|
|
13
|
+
rm,
|
|
14
|
+
writeFile,
|
|
15
|
+
} = require("node:fs/promises");
|
|
16
|
+
const { pipeline } = require("node:stream/promises");
|
|
17
|
+
const { Readable, Transform } = require("node:stream");
|
|
18
|
+
const path = require("node:path");
|
|
19
|
+
const { setTimeout: delay } = require("node:timers/promises");
|
|
20
|
+
const artifacts = require("./artifacts.json");
|
|
21
|
+
|
|
22
|
+
const runtimeError = (message) =>
|
|
23
|
+
Object.assign(Error(message), { status: 503 });
|
|
24
|
+
// Next.js can bundle workspace packages and rewrite __dirname. Locate the project
|
|
25
|
+
// from the service working directory so the panel and setup CLI share one runtime.
|
|
26
|
+
function findProjectRoot() {
|
|
27
|
+
let directory = process.cwd();
|
|
28
|
+
while (true) {
|
|
29
|
+
try {
|
|
30
|
+
const pkg = JSON.parse(
|
|
31
|
+
readFileSync(
|
|
32
|
+
/* turbopackIgnore: true */ path.join(directory, "package.json"),
|
|
33
|
+
"utf8",
|
|
34
|
+
),
|
|
35
|
+
);
|
|
36
|
+
if (
|
|
37
|
+
Array.isArray(pkg.workspaces) &&
|
|
38
|
+
pkg.workspaces.includes("apps/*") &&
|
|
39
|
+
pkg.workspaces.includes("packages/*")
|
|
40
|
+
)
|
|
41
|
+
return directory;
|
|
42
|
+
} catch {}
|
|
43
|
+
const parent = path.dirname(directory);
|
|
44
|
+
if (parent === directory)
|
|
45
|
+
throw runtimeError("Start Naviyra from its project directory");
|
|
46
|
+
directory = parent;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
const projectRoot = findProjectRoot();
|
|
50
|
+
function runtimeConfig({
|
|
51
|
+
platform = process.platform,
|
|
52
|
+
arch = process.arch,
|
|
53
|
+
root = process.env.NAVIYRA_RUNTIME_ROOT ||
|
|
54
|
+
path.join(projectRoot, ".runtime", "docker"),
|
|
55
|
+
} = {}) {
|
|
56
|
+
if (
|
|
57
|
+
!["linux", "win32"].includes(platform) ||
|
|
58
|
+
!["x64", "arm64"].includes(arch)
|
|
59
|
+
)
|
|
60
|
+
throw runtimeError(
|
|
61
|
+
"Naviyra Docker supports Windows/WSL2 and Linux on x64 or ARM64",
|
|
62
|
+
);
|
|
63
|
+
root = path.resolve(/* turbopackIgnore: true */ root);
|
|
64
|
+
const id = createHash("sha256")
|
|
65
|
+
.update(platform === "win32" ? root.toLowerCase() : root)
|
|
66
|
+
.digest("hex")
|
|
67
|
+
.slice(0, 10);
|
|
68
|
+
return {
|
|
69
|
+
platform,
|
|
70
|
+
arch: arch === "x64" ? "x86_64" : "aarch64",
|
|
71
|
+
root,
|
|
72
|
+
id,
|
|
73
|
+
distro: `Naviyra-${id}`,
|
|
74
|
+
linuxRoot: platform === "win32" ? "/opt/naviyra" : root,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
function cleanEnvironment(extra = {}) {
|
|
78
|
+
const env = { ...process.env };
|
|
79
|
+
for (const key of Object.keys(env))
|
|
80
|
+
if (key.startsWith("DOCKER_") || key === "WSLENV") delete env[key];
|
|
81
|
+
return { ...env, ...extra };
|
|
82
|
+
}
|
|
83
|
+
function execute(file, args, { env = {}, input, timeout = 30000 } = {}) {
|
|
84
|
+
return new Promise((resolve, reject) => {
|
|
85
|
+
const child = spawn(file, args, {
|
|
86
|
+
shell: false,
|
|
87
|
+
windowsHide: true,
|
|
88
|
+
env: cleanEnvironment(env),
|
|
89
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
90
|
+
});
|
|
91
|
+
const chunks = [];
|
|
92
|
+
let length = 0;
|
|
93
|
+
const timer = setTimeout(() => {
|
|
94
|
+
child.kill();
|
|
95
|
+
reject(runtimeError("Naviyra runtime operation timed out"));
|
|
96
|
+
}, timeout);
|
|
97
|
+
child.stdout.on("data", (chunk) => {
|
|
98
|
+
length += chunk.length;
|
|
99
|
+
if (length <= 1024 * 1024) chunks.push(chunk);
|
|
100
|
+
});
|
|
101
|
+
child.stderr.resume(); // Commands can output credentials. Do not propagate their output in errors.
|
|
102
|
+
child.stdin.on("error", () => {});
|
|
103
|
+
child.on("error", () => {
|
|
104
|
+
clearTimeout(timer);
|
|
105
|
+
reject(
|
|
106
|
+
runtimeError("Naviyra runtime could not launch a required OS command"),
|
|
107
|
+
);
|
|
108
|
+
});
|
|
109
|
+
child.on("close", (code) => {
|
|
110
|
+
clearTimeout(timer);
|
|
111
|
+
const bytes = Buffer.concat(chunks);
|
|
112
|
+
const result = bytes.includes(0)
|
|
113
|
+
? bytes.toString("utf16le")
|
|
114
|
+
: bytes.toString("utf8");
|
|
115
|
+
if (code === 0) resolve(result.trim());
|
|
116
|
+
else
|
|
117
|
+
reject(
|
|
118
|
+
runtimeError(
|
|
119
|
+
"Naviyra runtime command failed. Run npm run runtime:setup on the server for prerequisite checks.",
|
|
120
|
+
),
|
|
121
|
+
);
|
|
122
|
+
});
|
|
123
|
+
child.stdin.end(input);
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
const exists = async (file) => {
|
|
127
|
+
try {
|
|
128
|
+
await access(file);
|
|
129
|
+
return true;
|
|
130
|
+
} catch {
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
async function hashFile(file) {
|
|
135
|
+
const hash = createHash("sha256");
|
|
136
|
+
for await (const chunk of createReadStream(file)) hash.update(chunk);
|
|
137
|
+
return hash.digest("hex");
|
|
138
|
+
}
|
|
139
|
+
async function downloadArtifact(artifact, directory) {
|
|
140
|
+
await mkdir(directory, { recursive: true });
|
|
141
|
+
const destination = path.join(directory, artifact.file);
|
|
142
|
+
if (
|
|
143
|
+
(await exists(destination)) &&
|
|
144
|
+
(await hashFile(destination)) === artifact.sha256
|
|
145
|
+
)
|
|
146
|
+
return destination;
|
|
147
|
+
const temporary = `${destination}.${randomUUID()}.part`;
|
|
148
|
+
try {
|
|
149
|
+
const response = await fetch(artifact.url, {
|
|
150
|
+
signal: AbortSignal.timeout(600000),
|
|
151
|
+
redirect: "error",
|
|
152
|
+
});
|
|
153
|
+
if (!response.ok || !response.body) throw Error();
|
|
154
|
+
let length = 0;
|
|
155
|
+
const limit = new Transform({
|
|
156
|
+
transform(chunk, encoding, callback) {
|
|
157
|
+
length += chunk.length;
|
|
158
|
+
callback(
|
|
159
|
+
length > 250 * 1024 * 1024 ? Error("Download too large") : null,
|
|
160
|
+
chunk,
|
|
161
|
+
);
|
|
162
|
+
},
|
|
163
|
+
});
|
|
164
|
+
await pipeline(
|
|
165
|
+
Readable.fromWeb(response.body),
|
|
166
|
+
limit,
|
|
167
|
+
createWriteStream(temporary, { flags: "wx", mode: 0o600 }),
|
|
168
|
+
);
|
|
169
|
+
if ((await hashFile(temporary)) !== artifact.sha256) throw Error();
|
|
170
|
+
await rename(temporary, destination);
|
|
171
|
+
return destination;
|
|
172
|
+
} catch {
|
|
173
|
+
throw runtimeError(
|
|
174
|
+
`Could not download or verify ${artifact.file}. Check network access and retry.`,
|
|
175
|
+
);
|
|
176
|
+
} finally {
|
|
177
|
+
await rm(temporary, { force: true });
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function dockerCommand(config, args, env = {}) {
|
|
182
|
+
const binary = `${config.linuxRoot}/bin/docker`;
|
|
183
|
+
const command = [
|
|
184
|
+
binary,
|
|
185
|
+
"--config",
|
|
186
|
+
`${config.linuxRoot}/client`,
|
|
187
|
+
"--host",
|
|
188
|
+
`unix://${config.linuxRoot}/run/docker.sock`,
|
|
189
|
+
...args,
|
|
190
|
+
];
|
|
191
|
+
if (config.platform === "linux")
|
|
192
|
+
return { file: binary, args: command.slice(1), env };
|
|
193
|
+
const keys = Object.keys(env);
|
|
194
|
+
if (keys.some((key) => !/^[A-Z][A-Z0-9_]*$/.test(key)))
|
|
195
|
+
throw runtimeError("Invalid runtime environment key");
|
|
196
|
+
return {
|
|
197
|
+
file: "wsl.exe",
|
|
198
|
+
args: [
|
|
199
|
+
"--distribution",
|
|
200
|
+
config.distro,
|
|
201
|
+
"--user",
|
|
202
|
+
"root",
|
|
203
|
+
"--exec",
|
|
204
|
+
...command,
|
|
205
|
+
],
|
|
206
|
+
env: { ...env, WSLENV: keys.map((key) => `${key}/u`).join(":") },
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Cross-process lock shared by Next.js workers and the setup CLI. Never steal a live lock.
|
|
211
|
+
async function locked(config, task) {
|
|
212
|
+
const lock = path.join(config.root, "setup.lock");
|
|
213
|
+
await mkdir(config.root, { recursive: true });
|
|
214
|
+
for (let attempt = 0; attempt < 900; attempt++) {
|
|
215
|
+
try {
|
|
216
|
+
await mkdir(lock);
|
|
217
|
+
} catch (error) {
|
|
218
|
+
if (error.code !== "EEXIST") throw error;
|
|
219
|
+
try {
|
|
220
|
+
const pid = Number(await readFile(path.join(lock, "pid"), "utf8"));
|
|
221
|
+
if (Number.isInteger(pid) && pid > 0) {
|
|
222
|
+
try {
|
|
223
|
+
process.kill(pid, 0);
|
|
224
|
+
} catch (probe) {
|
|
225
|
+
if (probe.code === "ESRCH")
|
|
226
|
+
await rm(lock, { recursive: true, force: true });
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
} catch {}
|
|
230
|
+
await delay(1000);
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
try {
|
|
234
|
+
await writeFile(path.join(lock, "pid"), String(process.pid));
|
|
235
|
+
return await task();
|
|
236
|
+
} finally {
|
|
237
|
+
await rm(lock, { recursive: true, force: true });
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
throw runtimeError(
|
|
241
|
+
"Another Naviyra runtime setup is still running. Retry when it finishes.",
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function createManagedRuntime({
|
|
246
|
+
config = runtimeConfig(),
|
|
247
|
+
run = execute,
|
|
248
|
+
download = downloadArtifact,
|
|
249
|
+
sleep = delay,
|
|
250
|
+
} = {}) {
|
|
251
|
+
let starting;
|
|
252
|
+
const command = (args, options = {}) => {
|
|
253
|
+
const spec = dockerCommand(config, args, options.env);
|
|
254
|
+
return run(spec.file, spec.args, { ...options, env: spec.env });
|
|
255
|
+
};
|
|
256
|
+
const wsl = (args, options) =>
|
|
257
|
+
run(
|
|
258
|
+
"wsl.exe",
|
|
259
|
+
["-d", config.distro, "-u", "root", "--exec", ...args],
|
|
260
|
+
options,
|
|
261
|
+
);
|
|
262
|
+
async function info() {
|
|
263
|
+
const result = JSON.parse(
|
|
264
|
+
await command(["info", "--format", "{{json .}}"], { timeout: 10000 }),
|
|
265
|
+
);
|
|
266
|
+
if (
|
|
267
|
+
result.OSType !== "linux" ||
|
|
268
|
+
!result.Labels?.includes(`naviyra.runtime=${config.id}`)
|
|
269
|
+
)
|
|
270
|
+
throw Object.assign(
|
|
271
|
+
runtimeError(
|
|
272
|
+
"The private Docker endpoint does not belong to this Naviyra installation",
|
|
273
|
+
),
|
|
274
|
+
{ code: "RUNTIME_OWNERSHIP" },
|
|
275
|
+
);
|
|
276
|
+
return result;
|
|
277
|
+
}
|
|
278
|
+
async function bootstrap() {
|
|
279
|
+
const cache = path.join(projectRoot, ".runtime", "runtime-downloads");
|
|
280
|
+
if (config.platform === "win32") {
|
|
281
|
+
let distributions;
|
|
282
|
+
try {
|
|
283
|
+
distributions = await run("wsl.exe", ["--list", "--quiet"]);
|
|
284
|
+
} catch {
|
|
285
|
+
throw runtimeError(
|
|
286
|
+
"Enable WSL2 first: run wsl --install --no-distribution as Administrator, restart Windows if requested, then retry Naviyra setup.",
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
if (
|
|
290
|
+
!distributions
|
|
291
|
+
.split(/\r?\n/)
|
|
292
|
+
.map((line) => line.trim())
|
|
293
|
+
.includes(config.distro)
|
|
294
|
+
) {
|
|
295
|
+
const archive = await download(
|
|
296
|
+
artifacts[`alpine-${config.arch}`],
|
|
297
|
+
cache,
|
|
298
|
+
);
|
|
299
|
+
try {
|
|
300
|
+
await run(
|
|
301
|
+
"wsl.exe",
|
|
302
|
+
[
|
|
303
|
+
"--import",
|
|
304
|
+
config.distro,
|
|
305
|
+
path.join(config.root, "wsl"),
|
|
306
|
+
archive,
|
|
307
|
+
"--version",
|
|
308
|
+
"2",
|
|
309
|
+
],
|
|
310
|
+
{ timeout: 180000 },
|
|
311
|
+
);
|
|
312
|
+
} catch {
|
|
313
|
+
throw runtimeError(
|
|
314
|
+
"Could not create Naviyra's WSL2 environment. Enable virtualization and WSL2, then run npm run runtime:setup again.",
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
// Only Naviyra's dedicated distribution is modified; user distributions and Docker Desktop are never used.
|
|
319
|
+
try {
|
|
320
|
+
await wsl(
|
|
321
|
+
[
|
|
322
|
+
"sh",
|
|
323
|
+
"-c",
|
|
324
|
+
"apk add --no-cache ca-certificates iptables ip6tables procps util-linux",
|
|
325
|
+
],
|
|
326
|
+
{ timeout: 180000 },
|
|
327
|
+
);
|
|
328
|
+
} catch {
|
|
329
|
+
throw runtimeError(
|
|
330
|
+
"Could not install OS dependencies in Naviyra's WSL environment. Check Alpine repository access and retry.",
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
await wsl([
|
|
334
|
+
"mkdir",
|
|
335
|
+
"-p",
|
|
336
|
+
`${config.linuxRoot}/bin`,
|
|
337
|
+
`${config.linuxRoot}/run`,
|
|
338
|
+
`${config.linuxRoot}/client`,
|
|
339
|
+
]);
|
|
340
|
+
} else {
|
|
341
|
+
await mkdir(path.join(config.root, "bin"), { recursive: true });
|
|
342
|
+
await mkdir(path.join(config.root, "run"), {
|
|
343
|
+
recursive: true,
|
|
344
|
+
mode: 0o700,
|
|
345
|
+
});
|
|
346
|
+
await mkdir(path.join(config.root, "client"), { recursive: true });
|
|
347
|
+
}
|
|
348
|
+
const rootless = config.platform === "linux" && process.getuid?.() !== 0;
|
|
349
|
+
for (const name of rootless
|
|
350
|
+
? ["docker", "docker-rootless-extras"]
|
|
351
|
+
: ["docker"]) {
|
|
352
|
+
const archive = await download(
|
|
353
|
+
artifacts[`${name}-${config.arch}`],
|
|
354
|
+
cache,
|
|
355
|
+
);
|
|
356
|
+
if (config.platform === "win32") {
|
|
357
|
+
const mounted = await wsl(["wslpath", "-a", "-u", archive]);
|
|
358
|
+
await wsl([
|
|
359
|
+
"tar",
|
|
360
|
+
"-xzf",
|
|
361
|
+
mounted,
|
|
362
|
+
"--strip-components=1",
|
|
363
|
+
"-C",
|
|
364
|
+
`${config.linuxRoot}/bin`,
|
|
365
|
+
]);
|
|
366
|
+
} else
|
|
367
|
+
await run("tar", [
|
|
368
|
+
"-xzf",
|
|
369
|
+
archive,
|
|
370
|
+
"--strip-components=1",
|
|
371
|
+
"-C",
|
|
372
|
+
path.join(config.root, "bin"),
|
|
373
|
+
]);
|
|
374
|
+
}
|
|
375
|
+
const daemonConfig = JSON.stringify({
|
|
376
|
+
"data-root": `${config.linuxRoot}/data`,
|
|
377
|
+
"exec-root": `${config.linuxRoot}/run/exec`,
|
|
378
|
+
pidfile: `${config.linuxRoot}/run/docker.pid`,
|
|
379
|
+
hosts: [`unix://${config.linuxRoot}/run/docker.sock`],
|
|
380
|
+
labels: [`naviyra.runtime=${config.id}`],
|
|
381
|
+
bridge: "none",
|
|
382
|
+
"containerd-namespace": `naviyra-${config.id}`,
|
|
383
|
+
"containerd-plugins-namespace": `naviyra-plugins-${config.id}`,
|
|
384
|
+
features: { "containerd-snapshotter": false },
|
|
385
|
+
...(!rootless ? { group: "root" } : {}),
|
|
386
|
+
});
|
|
387
|
+
await writeFile(path.join(config.root, "daemon.json"), daemonConfig, {
|
|
388
|
+
mode: 0o600,
|
|
389
|
+
});
|
|
390
|
+
if (config.platform === "win32") {
|
|
391
|
+
const mounted = await wsl([
|
|
392
|
+
"wslpath",
|
|
393
|
+
"-a",
|
|
394
|
+
"-u",
|
|
395
|
+
path.join(config.root, "daemon.json"),
|
|
396
|
+
]);
|
|
397
|
+
await wsl(["cp", mounted, `${config.linuxRoot}/daemon.json`]);
|
|
398
|
+
}
|
|
399
|
+
await writeFile(
|
|
400
|
+
path.join(config.root, "installed.json"),
|
|
401
|
+
JSON.stringify({ version: "29.8.2", arch: config.arch }),
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
async function launch() {
|
|
405
|
+
const rootless = config.platform === "linux" && process.getuid?.() !== 0;
|
|
406
|
+
const bin = `${config.linuxRoot}/bin`;
|
|
407
|
+
const script =
|
|
408
|
+
'set -eu\nroot="$1"\nmode="$2"\nexport PATH="$root/bin:$PATH"\nexport XDG_RUNTIME_DIR="$root/run"\nexport DOCKERD_ROOTLESS_ROOTLESSKIT_STATE_DIR="$root/run/rootlesskit"\nif [ "$mode" = rootless ]; then\n command -v newuidmap >/dev/null && command -v newgidmap >/dev/null || exit 42\n daemon="$root/bin/dockerd-rootless.sh"\nelse\n daemon="$root/bin/dockerd"\nfi\nnohup "$daemon" --config-file "$root/daemon.json" >>"$root/daemon.log" 2>&1 </dev/null &\nsleep 2\n';
|
|
409
|
+
if (config.platform === "win32") {
|
|
410
|
+
await wsl([
|
|
411
|
+
"sh",
|
|
412
|
+
"-c",
|
|
413
|
+
script,
|
|
414
|
+
"naviyra-start",
|
|
415
|
+
config.linuxRoot,
|
|
416
|
+
"rootful",
|
|
417
|
+
]);
|
|
418
|
+
} else {
|
|
419
|
+
try {
|
|
420
|
+
await run(
|
|
421
|
+
"sh",
|
|
422
|
+
[
|
|
423
|
+
"-c",
|
|
424
|
+
script,
|
|
425
|
+
"naviyra-start",
|
|
426
|
+
config.linuxRoot,
|
|
427
|
+
rootless ? "rootless" : "rootful",
|
|
428
|
+
],
|
|
429
|
+
{ env: { PATH: `${bin}:${process.env.PATH}` } },
|
|
430
|
+
);
|
|
431
|
+
} catch {
|
|
432
|
+
throw runtimeError(
|
|
433
|
+
"Rootless Docker requires newuidmap/newgidmap (uidmap), subordinate UID/GID ranges and enabled user namespaces. Configure these OS prerequisites and retry runtime:setup.",
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
for (let attempt = 0; attempt < 60; attempt++) {
|
|
438
|
+
try {
|
|
439
|
+
await info();
|
|
440
|
+
return;
|
|
441
|
+
} catch {}
|
|
442
|
+
await sleep(1000);
|
|
443
|
+
}
|
|
444
|
+
throw runtimeError(
|
|
445
|
+
`Naviyra's Docker engine did not start. Check ${config.platform === "win32" ? `${config.distro}:/opt/naviyra/daemon.log` : path.join(config.root, "daemon.log")} and OS prerequisites.`,
|
|
446
|
+
);
|
|
447
|
+
}
|
|
448
|
+
async function ensure() {
|
|
449
|
+
try {
|
|
450
|
+
await info();
|
|
451
|
+
return;
|
|
452
|
+
} catch (error) {
|
|
453
|
+
if (error.code === "RUNTIME_OWNERSHIP") throw error;
|
|
454
|
+
}
|
|
455
|
+
if (!starting)
|
|
456
|
+
starting = locked(config, async () => {
|
|
457
|
+
try {
|
|
458
|
+
await info();
|
|
459
|
+
return;
|
|
460
|
+
} catch (error) {
|
|
461
|
+
if (error.code === "RUNTIME_OWNERSHIP") throw error;
|
|
462
|
+
}
|
|
463
|
+
if (!(await exists(path.join(config.root, "installed.json"))))
|
|
464
|
+
await bootstrap();
|
|
465
|
+
await launch();
|
|
466
|
+
}).finally(() => {
|
|
467
|
+
starting = undefined;
|
|
468
|
+
});
|
|
469
|
+
await starting;
|
|
470
|
+
}
|
|
471
|
+
return {
|
|
472
|
+
config,
|
|
473
|
+
ensure,
|
|
474
|
+
async run(args, options) {
|
|
475
|
+
await ensure();
|
|
476
|
+
return command(args, options);
|
|
477
|
+
},
|
|
478
|
+
async status() {
|
|
479
|
+
try {
|
|
480
|
+
const current = await info();
|
|
481
|
+
return {
|
|
482
|
+
ready: true,
|
|
483
|
+
version: current.ServerVersion,
|
|
484
|
+
platform: config.platform,
|
|
485
|
+
root: config.root,
|
|
486
|
+
};
|
|
487
|
+
} catch {
|
|
488
|
+
return { ready: false, platform: config.platform, root: config.root };
|
|
489
|
+
}
|
|
490
|
+
},
|
|
491
|
+
async stop() {
|
|
492
|
+
await locked(config, async () => {
|
|
493
|
+
await info(); // Never signal an engine without validating ownership.
|
|
494
|
+
const script =
|
|
495
|
+
'set -eu\nroot="$1"\npid=$(cat "$root/run/docker.pid")\ncase "$pid" in ""|*[!0-9]*) exit 1;; esac\nkill -TERM "$pid"\n';
|
|
496
|
+
if (config.platform === "win32")
|
|
497
|
+
await wsl(["sh", "-s", "--", config.linuxRoot], { input: script });
|
|
498
|
+
else await run("sh", ["-s", "--", config.linuxRoot], { input: script });
|
|
499
|
+
for (let attempt = 0; attempt < 30; attempt++) {
|
|
500
|
+
try {
|
|
501
|
+
await info();
|
|
502
|
+
} catch {
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
await sleep(1000);
|
|
506
|
+
}
|
|
507
|
+
throw runtimeError(
|
|
508
|
+
"The private engine is still shutting down. Check runtime:status before restarting.",
|
|
509
|
+
);
|
|
510
|
+
});
|
|
511
|
+
},
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
let managed;
|
|
515
|
+
function managedRuntime() {
|
|
516
|
+
return (managed ||= createManagedRuntime());
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
exports.runtimeError = runtimeError;
|
|
520
|
+
exports.runtimeConfig = runtimeConfig;
|
|
521
|
+
exports.cleanEnvironment = cleanEnvironment;
|
|
522
|
+
exports.execute = execute;
|
|
523
|
+
exports.downloadArtifact = downloadArtifact;
|
|
524
|
+
exports.dockerCommand = dockerCommand;
|
|
525
|
+
exports.createManagedRuntime = createManagedRuntime;
|
|
526
|
+
exports.managedRuntime = managedRuntime;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export const runtimeLabels = {
|
|
3
|
+
nodejs: "Node.js",
|
|
4
|
+
python: "Python",
|
|
5
|
+
php: "PHP",
|
|
6
|
+
go: "Go",
|
|
7
|
+
static: "Static / frontend",
|
|
8
|
+
custom: "Other (native executable)",
|
|
9
|
+
};
|
|
10
|
+
const relativePath = z
|
|
11
|
+
.string()
|
|
12
|
+
.max(200)
|
|
13
|
+
.regex(
|
|
14
|
+
/^(?!\/)(?!.*(?:^|\/)\.\.(?:\/|$))[a-zA-Z0-9_./-]+$/,
|
|
15
|
+
"Use a relative project path without ..",
|
|
16
|
+
);
|
|
17
|
+
export const runtimeConfig = z
|
|
18
|
+
.object({
|
|
19
|
+
python_server: z.enum(["asgi", "wsgi"]).default("asgi"),
|
|
20
|
+
entrypoint: z
|
|
21
|
+
.string()
|
|
22
|
+
.regex(/^[A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*:[A-Za-z_]\w*$/)
|
|
23
|
+
.default("app:app"),
|
|
24
|
+
public_dir: relativePath.default("."),
|
|
25
|
+
executable: z
|
|
26
|
+
.string()
|
|
27
|
+
.max(64)
|
|
28
|
+
.regex(/^[a-zA-Z0-9_.-]*$/)
|
|
29
|
+
.default(""),
|
|
30
|
+
args: z.array(z.string().max(1000)).max(32).default([]),
|
|
31
|
+
})
|
|
32
|
+
.default({
|
|
33
|
+
python_server: "asgi",
|
|
34
|
+
entrypoint: "app:app",
|
|
35
|
+
public_dir: ".",
|
|
36
|
+
executable: "",
|
|
37
|
+
args: [],
|
|
38
|
+
});
|
|
39
|
+
export const projectInput = z
|
|
40
|
+
.object({
|
|
41
|
+
type: z
|
|
42
|
+
.enum(["nodejs", "python", "php", "go", "static", "custom"])
|
|
43
|
+
.default("nodejs"),
|
|
44
|
+
runtime_config: runtimeConfig,
|
|
45
|
+
name: z
|
|
46
|
+
.string()
|
|
47
|
+
.min(2)
|
|
48
|
+
.max(48)
|
|
49
|
+
.regex(/^[a-z][a-z0-9-]+$/, "Use lowercase letters, numbers and hyphens"),
|
|
50
|
+
subdomain: z
|
|
51
|
+
.string()
|
|
52
|
+
.max(253)
|
|
53
|
+
.regex(
|
|
54
|
+
/^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}$/,
|
|
55
|
+
"Enter a full hostname, e.g. app.example.com",
|
|
56
|
+
),
|
|
57
|
+
source_type: z.enum(["git", "zip"]),
|
|
58
|
+
git_url: z.string().max(500).optional().default(""),
|
|
59
|
+
})
|
|
60
|
+
.superRefine((data, ctx) => {
|
|
61
|
+
if (data.source_type === "git") {
|
|
62
|
+
try {
|
|
63
|
+
const u = new URL(data.git_url);
|
|
64
|
+
if (
|
|
65
|
+
u.protocol !== "https:" ||
|
|
66
|
+
u.username ||
|
|
67
|
+
u.password ||
|
|
68
|
+
u.port ||
|
|
69
|
+
!["github.com", "gitlab.com", "bitbucket.org"].includes(u.hostname)
|
|
70
|
+
)
|
|
71
|
+
throw Error();
|
|
72
|
+
} catch {
|
|
73
|
+
ctx.addIssue({
|
|
74
|
+
code: "custom",
|
|
75
|
+
path: ["git_url"],
|
|
76
|
+
message:
|
|
77
|
+
"Use a public HTTPS GitHub, GitLab or Bitbucket repository URL",
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
export const idSchema = z.uuid();
|
|
83
|
+
export const deploymentInput = z.object({ deployment_id: z.uuid() });
|
|
84
|
+
export const MAX_ZIP_BYTES = 25 * 1024 * 1024;
|
|
85
|
+
export const statuses = ["created", "queued", "deploying", "running", "failed"];
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import pg from "pg";
|
|
2
|
+
import { readFile, readdir } from "node:fs/promises";
|
|
3
|
+
const client = new pg.Client({ connectionString: process.env.DATABASE_URL });
|
|
4
|
+
await client.connect();
|
|
5
|
+
try {
|
|
6
|
+
await client.query("BEGIN");
|
|
7
|
+
for (const file of (await readdir(new URL("../db/", import.meta.url)))
|
|
8
|
+
.filter((f) => f.endsWith(".sql"))
|
|
9
|
+
.sort()) {
|
|
10
|
+
await client.query(
|
|
11
|
+
await readFile(new URL(`../db/${file}`, import.meta.url), "utf8"),
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
await client.query("COMMIT");
|
|
15
|
+
console.log("Database schema ready.");
|
|
16
|
+
} finally {
|
|
17
|
+
await client.end();
|
|
18
|
+
}
|