Skip to content

Commit 826746f

Browse files
committed
getting basic podman to work
1 parent 27ba0a5 commit 826746f

File tree

7 files changed

+184
-18
lines changed

7 files changed

+184
-18
lines changed

src/packages/project-runner/bin/start.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,6 @@ const { init } = require("@cocalc/project-runner/run");
44

55
(async () => {
66
console.log("Starting...");
7-
await init({runtime:'nsjail'});
7+
await init();
88
console.log("CoCalc Project Runner ready");
99
})();

src/packages/project-runner/run/index.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export { type Configuration };
1414
import { init as initFilesystem } from "./filesystem";
1515
import getLogger from "@cocalc/backend/logger";
1616
import * as nsjail from "./nsjail";
17+
import * as podman from "./podman";
1718
import { init as initMounts } from "./mounts";
1819

1920
const logger = getLogger("project-runner:run");
@@ -22,14 +23,18 @@ let client: ConatClient | null = null;
2223
export async function init(
2324
opts: { client?: ConatClient; runtime?: "nsjail" | "podman" } = {},
2425
) {
25-
logger.debug("init", opts.runtime);
26+
const runtimeName = opts.runtime ?? "podman";
27+
logger.debug("init", runtimeName);
2628
let runtime;
27-
switch (opts.runtime) {
29+
switch (runtimeName) {
2830
case "nsjail":
2931
runtime = nsjail;
3032
break;
33+
case "podman":
34+
runtime = podman;
35+
break;
3136
default:
32-
throw Error(`runtime '${opts.runtime}' not implemented`);
37+
throw Error(`runtime '${runtimeName}' not implemented`);
3338
}
3439
client = opts.client ?? conat();
3540
initFilesystem({ client });

src/packages/project-runner/run/mounts.ts

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -27,19 +27,23 @@ export async function init() {
2727
}
2828
MOUNTS[type] = v;
2929
}
30-
MOUNTS["-R"].push(`${dirname(root)}:/cocalc`);
31-
32-
// also if this node is install via nvm, we make exactly this
33-
// version of node's install available
34-
if (!process.execPath.startsWith("/usr/")) {
35-
// not already in an obvious system-wide place we included above
36-
// IMPORTANT: take care not to put the binary next to sensitive info!
37-
MOUNTS["-R"].push(`${dirname(process.execPath)}:/cocalc/bin`);
38-
nodePath = join("/cocalc/bin", basename(process.execPath));
30+
const cocalcMounts = getCoCalcMounts();
31+
for (const path in cocalcMounts) {
32+
MOUNTS[path] = cocalcMounts[path];
3933
}
4034
logger.debug(MOUNTS);
4135
}
4236

37+
export function getCoCalcMounts() {
38+
nodePath = join("/cocalc/bin", basename(process.execPath));
39+
// IMPORTANT: take care not to put the binary next to sensitive info due
40+
// to mapping in process.execPath!
41+
return {
42+
[dirname(root)]: "/cocalc",
43+
[dirname(process.execPath)]: "/cocalc/bin",
44+
};
45+
}
46+
4347
export async function getMounts() {
4448
await init();
4549
return MOUNTS;

src/packages/project-runner/run/nsjail.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ import { once } from "@cocalc/util/async-utils";
3333
import { getMounts } from "./mounts";
3434
import { mountHome, setQuota } from "./filesystem";
3535

36-
// for development it may be useful to just disabling using nsjail namespaces
36+
// for development it may be useful to just disable using nsjail namespaces
3737
// entirely -- change this to true to do so.
3838
const DISABLE_NSJAIL = false;
3939

@@ -63,7 +63,7 @@ export async function start({
6363
let uid, gid;
6464
if (userInfo().uid) {
6565
// server running as non-root user -- single user mode
66-
uid = gid = userInfo().uid;
66+
({ uid, gid } = userInfo());
6767
} else {
6868
// server is running as root -- multiuser mode
6969
uid = gid = DEFAULT_UID;
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
/*
2+
Runner based on podman.
3+
*/
4+
5+
import getLogger from "@cocalc/backend/logger";
6+
import { nodePath } from "./mounts";
7+
import { isValidUUID } from "@cocalc/util/misc";
8+
import { ensureConfFilesExists, setupDataPath, writeSecretToken } from "./util";
9+
import { getEnvironment } from "./env";
10+
import { mkdir } from "fs/promises";
11+
import { spawn } from "node:child_process";
12+
import { type Configuration } from "./types";
13+
export { type Configuration };
14+
import { getCoCalcMounts } from "./mounts";
15+
import { mountHome, setQuota } from "./filesystem";
16+
import { executeCode } from "@cocalc/backend/execute-code";
17+
18+
const logger = getLogger("project-runner:podman");
19+
const children: { [project_id: string]: any } = {};
20+
21+
const GRACE_PERIOD = 2000;
22+
23+
const DEFAULT_IMAGE = "ubuntu:25.04";
24+
25+
export async function start({
26+
project_id,
27+
config,
28+
}: {
29+
project_id: string;
30+
config?: Configuration;
31+
}) {
32+
if (!isValidUUID(project_id)) {
33+
throw Error("start: project_id must be valid");
34+
}
35+
logger.debug("start", { project_id, config: { ...config, secret: "xxx" } });
36+
if (children[project_id] != null && children[project_id].exitCode == null) {
37+
logger.debug("start -- already running");
38+
return;
39+
}
40+
41+
const home = await mountHome(project_id);
42+
await mkdir(home, { recursive: true });
43+
await ensureConfFilesExists(home);
44+
const env = getEnvironment({
45+
project_id,
46+
env: config?.env,
47+
HOME: "/home/user",
48+
});
49+
await setupDataPath(home);
50+
if (config?.secret) {
51+
await writeSecretToken(home, config.secret);
52+
}
53+
54+
if (config?.disk) {
55+
// TODO: maybe this should be done in parallel with other things
56+
// to make startup time slightly faster (?) -- could also be incorporated
57+
// into mount.
58+
await setQuota(project_id, config.disk);
59+
}
60+
61+
const args: string[] = ["run", "--rm", "--network=host", "--user=0:0"];
62+
const cmd = "podman";
63+
const script = "/cocalc/src/packages/project/bin/cocalc-project.js";
64+
65+
args.push("--hostname", `project-${project_id}`);
66+
args.push("--name", `project-${project_id}`);
67+
68+
const mounts = getCoCalcMounts();
69+
for (const path in mounts) {
70+
args.push("-v", `${path}:${mounts[path]}:ro`);
71+
}
72+
args.push("-v", `${home}:${env.HOME}`);
73+
for (const name in env) {
74+
args.push("-e", `${name}=${env[name]}`);
75+
}
76+
77+
args.push(config?.image ?? DEFAULT_IMAGE);
78+
args.push(nodePath);
79+
args.push(script, "--init", "project_init.sh");
80+
81+
console.log(`${cmd} ${args.join(" ")}`);
82+
logger.debug(`${cmd} ${args.join(" ")}`);
83+
84+
const child = spawn(cmd, args);
85+
children[project_id] = child;
86+
87+
child.stdout.on("data", (chunk: Buffer) => {
88+
logger.debug(`project_id=${project_id}.stdout: `, chunk.toString());
89+
});
90+
child.stderr.on("data", (chunk: Buffer) => {
91+
logger.debug(`project_id=${project_id}.stderr: `, chunk.toString());
92+
});
93+
}
94+
95+
export async function stop({ project_id }) {
96+
if (!isValidUUID(project_id)) {
97+
throw Error("stop: project_id must be valid");
98+
}
99+
logger.debug("stop", { project_id });
100+
const child = children[project_id];
101+
if (child != null && child.exitCode == null) {
102+
try {
103+
await podman([
104+
"stop",
105+
"-t",
106+
`${GRACE_PERIOD / 1000}`,
107+
`project-${project_id}`,
108+
]);
109+
await podman(["rm", `project-${project_id}`]);
110+
} catch {}
111+
delete children[project_id];
112+
}
113+
}
114+
115+
async function podman(args: string[]) {
116+
return await executeCode({ command: "podman", args, err_on_exit: true });
117+
}
118+
119+
async function state(project_id) {
120+
const { stdout } = await podman([
121+
"ps",
122+
"--filter",
123+
`name=project-${project_id}`,
124+
"--format",
125+
"{{.State}}",
126+
]);
127+
return stdout.trim() == "running" ? "running" : "opened";
128+
}
129+
130+
export async function status({ project_id }) {
131+
if (!isValidUUID(project_id)) {
132+
throw Error("status: project_id must be valid");
133+
}
134+
logger.debug("status", { project_id });
135+
// TODO
136+
return { state: await state(project_id), ip: "127.0.0.1" };
137+
}
138+
139+
export async function close() {
140+
const v: any[] = [];
141+
for (const project_id in children) {
142+
logger.debug(`killing project_id=${project_id}`);
143+
v.push(stop({ project_id }));
144+
delete children[project_id];
145+
}
146+
await Promise.all(v);
147+
}
148+
149+
// important because it kills all
150+
// the processes that were spawned
151+
process.once("exit", close);
152+
["SIGINT", "SIGTERM", "SIGQUIT"].forEach((sig) => {
153+
process.once(sig, () => {
154+
process.exit();
155+
});
156+
});

src/packages/project-runner/run/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
export interface Configuration {
2+
// optional Docker image
3+
image?: string;
24
// shared secret between project and hubs to enhance security (via defense in depth)
35
secret?: string;
46
// extra variables that get merged into the environment of the project.

src/packages/server/conat/project/run.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,10 @@ const logger = getLogger("server:conat:project:run");
2121

2222
const servers: any[] = [];
2323
export async function init(count: number = 1) {
24-
const opts = { runtime: "nsjail" as "nsjail" };
25-
logger.debug("init project runner(s)", { count, opts });
24+
logger.debug("init project runner(s)", { count });
2625
await loadConatConfiguration();
2726
for (let i = 0; i < count; i++) {
28-
const server = await initProjectRunner(opts);
27+
const server = await initProjectRunner();
2928
servers.push(server);
3029
}
3130
}

0 commit comments

Comments
 (0)