Skip to content

Commit c8d8465

Browse files
committed
project-runner: quick proof of concept of rsync first
1 parent cbcf18b commit c8d8465

File tree

11 files changed

+111
-40
lines changed

11 files changed

+111
-40
lines changed

src/packages/backend/data.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -288,7 +288,7 @@ export const sshServer: { name: string; host: string; port: number } = (() => {
288288
process.env.COCALC_SSH_SERVER ?? "host.containers.internal"
289289
).split(":");
290290
return {
291-
name: "cocalc-core",
291+
name: "file-server",
292292
host: host ? host : "host.containers.internal",
293293
port: parseInt(port),
294294
};

src/packages/backend/mutagen/ssh-keys.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ Host ${name}
4848
}
4949
if (!config.includes(hostConfig)) {
5050
// put at front since only the first with a given name is used by ssh
51-
await writeFile(configPath, hostConfig + "\n" + config);
51+
await writeFile(configPath, hostConfig + "\n" + config, { mode: 0o700 });
5252
}
5353
}
5454
}

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

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,18 @@ export interface Options {
3939
sshServers?: SshServersFunction;
4040
}) => Promise<void>;
4141
// ensure a specific project is not running on this runner
42-
stop: (opts: { project_id: string }) => Promise<void>;
42+
stop: (opts: {
43+
project_id: string;
44+
localPath: LocalPathFunction;
45+
sshServers?: SshServersFunction;
46+
}) => Promise<void>;
4347
// get the status of a project here.
4448

45-
status: (opts: { project_id: string }) => Promise<ProjectStatus>;
49+
status: (opts: {
50+
project_id: string;
51+
localPath: LocalPathFunction;
52+
sshServers?: SshServersFunction;
53+
}) => Promise<ProjectStatus>;
4654
// local -- the absolute path on the filesystem where the home directory of this
4755
// project is hosted. In case of a single server setup it could be the exact
4856
// same path as the remote files and no sync is involved.
@@ -110,14 +118,25 @@ export async function server(options: Options) {
110118
async stop(opts: { project_id: string }) {
111119
logger.debug("stop", opts.project_id);
112120
projects.set(opts.project_id, { server: id, state: "stopping" } as const);
113-
await stop(opts);
121+
await stop({
122+
...opts,
123+
localPath: options.localPath,
124+
sshServers: options.sshServers,
125+
});
114126
const s = { server: id, state: "opened" } as const;
115127
projects.set(opts.project_id, s);
116128
return s;
117129
},
118130
async status(opts: { project_id: string }) {
119131
logger.debug("status", opts.project_id);
120-
const s = { ...(await status(opts)), server: id };
132+
const s = {
133+
...(await status({
134+
...opts,
135+
localPath: options.localPath,
136+
sshServers: options.sshServers,
137+
})),
138+
server: id,
139+
};
121140
projects.set(opts.project_id, s);
122141
return s;
123142
},
@@ -134,11 +153,18 @@ export async function server(options: Options) {
134153

135154
export function client({
136155
client,
156+
project_id,
137157
subject,
158+
timeout,
159+
waitForInterest = true,
138160
}: {
139161
client?: Client;
140-
subject: string;
162+
project_id?: string;
163+
subject?: string;
164+
timeout?: number;
165+
waitForInterest?: boolean;
141166
}): API {
167+
subject ??= `project.${project_id}.run`;
142168
client ??= conat();
143-
return client.call<API>(subject, { waitForInterest: true });
169+
return client.call<API>(subject, { waitForInterest, timeout });
144170
}

src/packages/conat/project/runner/state.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ export type ProjectState = "running" | "opened" | "stopping" | "starting";
1010
export interface ProjectStatus {
1111
server?: string;
1212
state: ProjectState;
13-
ip?: string; // the ip address when running
13+
publicKey?: string; // ed25519 ssh public key
1414
}
1515

1616
export default async function state({ client }) {

src/packages/file-server/ssh/auth.ts

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
client as createFileClient,
1313
type Fileserver,
1414
} from "@cocalc/conat/files/file-server";
15+
import { client as projectRunnerClient } from "@cocalc/conat/project/runner/run";
1516

1617
const logger = getLogger("file-server:ssh:auth");
1718

@@ -98,13 +99,28 @@ async function handleRequest(
9899
const volume = `project-${project_id}`;
99100
const id = user.slice("project-".length + 37);
100101
const compute_server_id = parseInt(id ? id : "0");
101-
const api = projectApiClient({
102-
project_id,
103-
compute_server_id,
104-
client,
105-
timeout: 5000,
106-
});
107-
const authorizedKeys = await api.system.sshPublicKey();
102+
let authorizedKeys;
103+
if (!compute_server_id) {
104+
const runner = projectRunnerClient({
105+
client,
106+
project_id,
107+
timeout: 5000,
108+
waitForInterest: false,
109+
});
110+
const s = await runner.status({ project_id });
111+
authorizedKeys = s.publicKey;
112+
if (!authorizedKeys) {
113+
throw Error("no ssh key known");
114+
}
115+
} else {
116+
const api = projectApiClient({
117+
project_id,
118+
compute_server_id,
119+
client,
120+
timeout: 5000,
121+
});
122+
authorizedKeys = await api.system.sshPublicKey();
123+
}
108124

109125
// NOTE/TODO: we could have a special username that maps to a
110126
// specific path in a project, which would change this path here,

src/packages/frontend/conat/client.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -473,7 +473,7 @@ export class ConatClient extends EventEmitter {
473473

474474
projectRunner = (project_id: string) => {
475475
return projectRunnerClient({
476-
subject: `project.${project_id}.run`,
476+
project_id,
477477
client: this.conat(),
478478
});
479479
};

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import {
44
} from "@cocalc/conat/files/file-server";
55
import { type Client as ConatClient } from "@cocalc/conat/core/client";
66
import { sshServer as defaultSshServer } from "@cocalc/backend/data";
7+
import { join } from "node:path";
8+
import { mkdir } from "node:fs/promises";
79

810
//import getLogger from "@cocalc/backend/logger";
911

@@ -37,6 +39,11 @@ export async function localPath({
3739
}: {
3840
project_id: string;
3941
}): Promise<string> {
42+
if (process.env.COCALC_PROJECT_PATH) {
43+
const path = join(process.env.COCALC_PROJECT_PATH, project_id);
44+
await mkdir(path, { recursive: true });
45+
return path;
46+
}
4047
const c = getFsClient();
4148
const { path } = await c.mount({ project_id });
4249
return path;

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

Lines changed: 41 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import { nodePath } from "./mounts";
2020
import { isValidUUID } from "@cocalc/util/misc";
2121
import { ensureConfFilesExists, setupDataPath, writeSecretToken } from "./util";
2222
import { getEnvironment } from "./env";
23-
import { mkdir } from "fs/promises";
23+
import { mkdir, readFile } from "node:fs/promises";
2424
import { spawn } from "node:child_process";
2525
import { getCoCalcMounts, COCALC_SRC } from "./mounts";
2626
import { setQuota } from "./filesystem";
@@ -76,27 +76,31 @@ export async function start({
7676

7777
const home = await localPath({ project_id });
7878
logger.debug("start: got home", { project_id, home });
79+
const mounts = getCoCalcMounts();
80+
const image = getImage(config);
81+
await initSshKeys({ home, sshServers: await sshServers?.({ project_id }) });
82+
83+
const env = await getEnvironment({
84+
project_id,
85+
env: config?.env,
86+
HOME: "/root",
87+
image,
88+
});
89+
await startSidecar({ project_id, home, mounts, env, pod });
90+
7991
const rootfs = await rootFilesystem.mount({ project_id, home, config });
8092
logger.debug("start: got rootfs", { project_id, rootfs });
8193
await mkdir(home, { recursive: true });
8294
logger.debug("start: created home", { project_id });
8395
await ensureConfFilesExists(home);
8496
logger.debug("start: created conf files", { project_id });
85-
const image = getImage(config);
8697

8798
await writeMutagenConfig({
8899
home,
89100
sync: config?.sync,
90101
forward: config?.forward,
91102
});
92-
await initSshKeys({ home, sshServers: await sshServers?.({ project_id }) });
93103

94-
const env = await getEnvironment({
95-
project_id,
96-
env: config?.env,
97-
HOME: "/root",
98-
image,
99-
});
100104
await setupDataPath(home);
101105
logger.debug("start: setup data path", { project_id });
102106
if (config?.secret) {
@@ -124,7 +128,6 @@ export async function start({
124128
args.push("--hostname", `project-${project_id}`);
125129
args.push("--name", `project-${project_id}`);
126130

127-
const mounts = getCoCalcMounts();
128131
for (const path in mounts) {
129132
args.push("-v", `${path}:${mounts[path]}:ro`);
130133
}
@@ -154,10 +157,14 @@ export async function start({
154157
child.stderr.on("data", (chunk: Buffer) => {
155158
logger.debug(`project_id=${project_id}.stderr: `, chunk.toString());
156159
});
160+
}
157161

162+
async function startSidecar({ project_id, mounts, env, pod, home }) {
163+
// sidecar: refactor
164+
const sidecarPodName = `sidecar-${project_id}`;
158165
const args2 = [
159166
"run",
160-
`--name=sidecar-${project_id}`,
167+
`--name=${sidecarPodName}`,
161168
"--detach",
162169
"--rm",
163170
"--pod",
@@ -172,11 +179,21 @@ export async function start({
172179
args2.push("-e", `${name}=${env[name]}`);
173180
}
174181
args2.push(sidecarImageName, "mutagen", "daemon", "run");
175-
// TODO: we don't block the startup for this, but we do
176-
// need to check and provide info to the user about what happened.
177-
(async () => {
178-
await podman(args2);
179-
})();
182+
await podman(args2);
183+
try {
184+
await podman([
185+
"exec",
186+
sidecarPodName,
187+
"rsync",
188+
"-axH",
189+
"--exclude=.mutagen",
190+
"--delete",
191+
"file-server:/root/",
192+
"/root/",
193+
]);
194+
} catch (err) {
195+
console.log(err);
196+
}
180197
}
181198

182199
export async function stop({
@@ -265,13 +282,18 @@ async function state(project_id): Promise<ProjectState> {
265282
return status == "running" ? "running" : "opened";
266283
}
267284

268-
export async function status({ project_id }) {
285+
export async function status({ project_id, localPath }) {
269286
if (!isValidUUID(project_id)) {
270287
throw Error("status: project_id must be valid");
271288
}
272289
logger.debug("status", { project_id });
273-
// TODO
274-
return { state: await state(project_id), ip: "127.0.0.1" };
290+
const s = await state(project_id);
291+
let publicKey: string | undefined = undefined;
292+
const home = await localPath({ project_id });
293+
try {
294+
publicKey = await readFile(join(home, ".ssh", "id_ed25519.pub"), "utf8");
295+
} catch {}
296+
return { state: s, ip: "127.0.0.1", publicKey };
275297
}
276298

277299
export async function close() {

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,10 @@ import { build } from "@cocalc/backend/podman/build-container";
3232

3333
const Dockerfile = `
3434
FROM docker.io/alpine:latest
35-
RUN apk update && apk add --no-cache openssh-client
35+
RUN apk update && apk add --no-cache openssh-client rsync
3636
`;
3737

38-
export const sidecarImageName = "localhost/sidecar:0.2";
38+
export const sidecarImageName = "localhost/sidecar:0.3";
3939

4040
export async function init() {
4141
await build({ name: sidecarImageName, Dockerfile });

src/packages/server/conat/project/load-balancer.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { loadConatConfiguration } from "../configuration";
1111
import getLogger from "@cocalc/backend/logger";
1212
import getPool from "@cocalc/database/pool";
1313
import { getProject } from "@cocalc/server/projects/control";
14-
import { type Configuration } from "@cocalc/project-runner/run";
14+
import { type Configuration } from "@cocalc/conat/project/runner/types";
1515
import { getProjectSecretToken } from "@cocalc/server/projects/control/secret-token";
1616

1717
const logger = getLogger("server:conat:project:load-balancer");

0 commit comments

Comments
 (0)