Preliminary containerization work

This commit is contained in:
Radon Rosborough 2021-03-19 23:04:59 -07:00
parent 83208355d4
commit b99d17bcd3
19 changed files with 368 additions and 510 deletions

View File

@ -72,11 +72,13 @@ ifneq (,$(filter $(I),admin ci))
docker run -it --rm --hostname $(I) -v $(VOLUME_MOUNT):/src -v /var/run/docker.sock:/var/run/docker.sock -v $(HOME)/.aws:/var/riju/.aws -v $(HOME)/.docker:/var/riju/.docker -v $(HOME)/.ssh:/var/riju/.ssh -v $(HOME)/.terraform.d:/var/riju/.terraform.d -e AWS_REGION -e AWS_ACCESS_KEY_ID -e AWS_SECRET_ACCESS_KEY -e DOCKER_USERNAME -e DOCKER_PASSWORD -e DEPLOY_SSH_PRIVATE_KEY -e DOCKER_REPO -e S3_BUCKET -e DOMAIN -e VOLUME_MOUNT=$(VOLUME_MOUNT) $(SHELL_PORTS) $(SHELL_ENV) --network host riju:$(I) $(BASH_CMD) docker run -it --rm --hostname $(I) -v $(VOLUME_MOUNT):/src -v /var/run/docker.sock:/var/run/docker.sock -v $(HOME)/.aws:/var/riju/.aws -v $(HOME)/.docker:/var/riju/.docker -v $(HOME)/.ssh:/var/riju/.ssh -v $(HOME)/.terraform.d:/var/riju/.terraform.d -e AWS_REGION -e AWS_ACCESS_KEY_ID -e AWS_SECRET_ACCESS_KEY -e DOCKER_USERNAME -e DOCKER_PASSWORD -e DEPLOY_SSH_PRIVATE_KEY -e DOCKER_REPO -e S3_BUCKET -e DOMAIN -e VOLUME_MOUNT=$(VOLUME_MOUNT) $(SHELL_PORTS) $(SHELL_ENV) --network host riju:$(I) $(BASH_CMD)
else ifeq ($(I),app) else ifeq ($(I),app)
docker run -it --rm --hostname $(I) $(SHELL_PORTS) $(SHELL_ENV) riju:$(I) $(BASH_CMD) docker run -it --rm --hostname $(I) $(SHELL_PORTS) $(SHELL_ENV) riju:$(I) $(BASH_CMD)
else ifneq (,$(filter $(I),runtime lang)) else ifneq (,$(filter $(I),base lang))
ifeq ($(I),lang) ifeq ($(I),lang)
@: $${L} @: $${L}
endif endif
docker run -it --rm --hostname $(I) -v $(VOLUME_MOUNT):/src --label riju-install-target=yes $(SHELL_PORTS) $(SHELL_ENV) riju:$(LANG_TAG) $(BASH_CMD) docker run -it --rm --hostname $(I) -v $(VOLUME_MOUNT):/src --label riju-install-target=yes $(SHELL_PORTS) $(SHELL_ENV) riju:$(LANG_TAG) $(BASH_CMD)
else ifeq ($(I),runtime)
docker run -it --rm --hostname $(I) -v $(VOLUME_MOUNT):/src -v /var/run/docker.sock:/var/run/docker.sock $(SHELL_PORTS) $(SHELL_ENV) riju:$(I) $(BASH_CMD)
else else
docker run -it --rm --hostname $(I) -v $(VOLUME_MOUNT):/src $(SHELL_PORTS) $(SHELL_ENV) riju:$(I) $(BASH_CMD) docker run -it --rm --hostname $(I) -v $(VOLUME_MOUNT):/src $(SHELL_PORTS) $(SHELL_ENV) riju:$(I) $(BASH_CMD)
endif endif

View File

@ -6,12 +6,10 @@ import pty from "node-pty";
import pQueue from "p-queue"; import pQueue from "p-queue";
const PQueue = pQueue.default; const PQueue = pQueue.default;
import rpc from "vscode-jsonrpc"; import rpc from "vscode-jsonrpc";
import { v4 as getUUID } from "uuid";
import { langs } from "./langs.js"; import { langs } from "./langs.js";
import { borrowUser } from "./users.js";
import * as util from "./util.js"; import * as util from "./util.js";
import { bash } from "./util.js"; import { bash, getUUID } from "./util.js";
const allSessions = new Set(); const allSessions = new Set();
@ -24,16 +22,8 @@ export class Session {
return langs[this.lang]; return langs[this.lang];
} }
get uid() {
return this.uidInfo.uid;
}
returnUser = async () => {
this.uidInfo && (await this.uidInfo.returnUser());
};
get context() { get context() {
return { uid: this.uid, uuid: this.uuid }; return { uuid: this.uuid, lang: this.lang };
} }
log = (msg) => this.logPrimitive(`[${this.uuid}] ${msg}`); log = (msg) => this.logPrimitive(`[${this.uuid}] ${msg}`);
@ -43,7 +33,7 @@ export class Session {
this.uuid = getUUID(); this.uuid = getUUID();
this.lang = lang; this.lang = lang;
this.tearingDown = false; this.tearingDown = false;
this.uidInfo = null; this.container = null;
this.term = null; this.term = null;
this.lsp = null; this.lsp = null;
this.daemon = null; this.daemon = null;
@ -57,24 +47,48 @@ export class Session {
return await util.run(args, this.log, options); return await util.run(args, this.log, options);
}; };
privilegedSetup = () => util.privilegedSetup(this.context); privilegedSession = () => util.privilegedSession(this.context);
privilegedSpawn = (args) => util.privilegedSpawn(this.context, args); privilegedWait = () => util.privilegedWait(this.context);
privilegedUseradd = () => util.privilegedUseradd(this.uid); privilegedExec = (args) => util.privilegedExec(this.context, args);
privilegedTeardown = () => util.privilegedTeardown(this.context);
setup = async () => { setup = async () => {
try { try {
allSessions.add(this); allSessions.add(this);
const { uid, returnUser } = await borrowUser(); const containerArgs = this.privilegedSession();
this.uidInfo = { uid, returnUser }; const containerProc = spawn(containerArgs[0], containerArgs.slice(1));
this.log(`Borrowed uid ${this.uid}`); this.container = {
await this.run(this.privilegedSetup()); proc: containerProc,
};
for (const stream of [containerProc.stdout, containerProc.stderr]) {
stream.on("data", (data) =>
this.send({
event: "serviceLog",
service: "container",
output: data.toString("utf8"),
})
);
containerProc.on("close", (code, signal) =>
this.send({
event: "serviceFailed",
service: "container",
error: `Exited with status ${signal || code}`,
})
);
containerProc.on("error", (err) =>
this.send({
event: "serviceFailed",
service: "container",
error: `${err}`,
})
);
}
await this.run(this.privilegedWait(this.context));
if (this.config.setup) { if (this.config.setup) {
await this.run(this.privilegedSpawn(bash(this.config.setup))); await this.run(this.privilegedExec(bash(this.config.setup)));
} }
await this.runCode(); await this.runCode();
if (this.config.daemon) { if (this.config.daemon) {
const daemonArgs = this.privilegedSpawn(bash(this.config.daemon)); const daemonArgs = this.privilegedExec(bash(this.config.daemon));
const daemonProc = spawn(daemonArgs[0], daemonArgs.slice(1)); const daemonProc = spawn(daemonArgs[0], daemonArgs.slice(1));
this.daemon = { this.daemon = {
proc: daemonProc, proc: daemonProc,
@ -105,9 +119,9 @@ export class Session {
} }
if (this.config.lsp) { if (this.config.lsp) {
if (this.config.lsp.setup) { if (this.config.lsp.setup) {
await this.run(this.privilegedSpawn(bash(this.config.lsp.setup))); await this.run(this.privilegedExec(bash(this.config.lsp.setup)));
} }
const lspArgs = this.privilegedSpawn(bash(this.config.lsp.start)); const lspArgs = this.privilegedExec(bash(this.config.lsp.start));
const lspProc = spawn(lspArgs[0], lspArgs.slice(1)); const lspProc = spawn(lspArgs[0], lspArgs.slice(1));
this.lsp = { this.lsp = {
proc: lspProc, proc: lspProc,
@ -252,7 +266,7 @@ export class Session {
writeCode = async (code) => { writeCode = async (code) => {
if (this.config.main.includes("/")) { if (this.config.main.includes("/")) {
await this.run( await this.run(
this.privilegedSpawn([ this.privilegedExec([
"mkdir", "mkdir",
"-p", "-p",
path.dirname(`${this.homedir}/${this.config.main}`), path.dirname(`${this.homedir}/${this.config.main}`),
@ -260,7 +274,7 @@ export class Session {
); );
} }
await this.run( await this.run(
this.privilegedSpawn([ this.privilegedExec([
"sh", "sh",
"-c", "-c",
`cat > ${path.resolve(this.homedir, this.config.main)}`, `cat > ${path.resolve(this.homedir, this.config.main)}`,
@ -283,7 +297,7 @@ export class Session {
} = this.config; } = this.config;
if (this.term) { if (this.term) {
const pid = this.term.pty.pid; const pid = this.term.pty.pid;
const args = this.privilegedSpawn( const args = this.privilegedExec(
bash(`kill -SIGTERM ${pid}; sleep 1; kill -SIGKILL ${pid}`) bash(`kill -SIGTERM ${pid}; sleep 1; kill -SIGKILL ${pid}`)
); );
spawn(args[0], args.slice(1)); spawn(args[0], args.slice(1));
@ -310,7 +324,7 @@ export class Session {
code += suffix + "\n"; code += suffix + "\n";
} }
await this.writeCode(code); await this.writeCode(code);
const termArgs = this.privilegedSpawn(bash(cmdline)); const termArgs = this.privilegedExec(bash(cmdline));
const term = { const term = {
pty: pty.spawn(termArgs[0], termArgs.slice(1), { pty: pty.spawn(termArgs[0], termArgs.slice(1), {
name: "xterm-color", name: "xterm-color",
@ -349,14 +363,14 @@ export class Session {
} }
if (this.formatter) { if (this.formatter) {
const pid = this.formatter.proc.pid; const pid = this.formatter.proc.pid;
const args = this.privilegedSpawn( const args = this.privilegedExec(
bash(`kill -SIGTERM ${pid}; sleep 1; kill -SIGKILL ${pid}`) bash(`kill -SIGTERM ${pid}; sleep 1; kill -SIGKILL ${pid}`)
); );
spawn(args[0], args.slice(1)); spawn(args[0], args.slice(1));
this.formatter.live = false; this.formatter.live = false;
this.formatter = null; this.formatter = null;
} }
const args = this.privilegedSpawn(bash(this.config.format.run)); const args = this.privilegedExec(bash(this.config.format.run));
const formatter = { const formatter = {
proc: spawn(args[0], args.slice(1)), proc: spawn(args[0], args.slice(1)),
live: true, live: true,
@ -409,7 +423,7 @@ export class Session {
}; };
ensure = async (cmd) => { ensure = async (cmd) => {
const code = await this.run(this.privilegedSpawn(bash(cmd)), { const code = await this.run(this.privilegedExec(bash(cmd)), {
check: false, check: false,
}); });
this.send({ event: "ensured", code }); this.send({ event: "ensured", code });
@ -422,11 +436,15 @@ export class Session {
} }
this.log(`Tearing down session`); this.log(`Tearing down session`);
this.tearingDown = true; this.tearingDown = true;
allSessions.delete(this); if (this.container) {
if (this.uidInfo) { // SIGTERM should be sufficient as the command running in the
await this.run(this.privilegedTeardown()); // foreground is just 'tail -f /dev/null' which won't try to
await this.returnUser(); // block signals. Killing the foreground process (i.e. pid1)
// should cause the Docker runtime to bring everything else
// down in flames.
this.container.proc.kill();
} }
allSessions.delete(this);
this.ws.terminate(); this.ws.terminate();
} catch (err) { } catch (err) {
this.log(`Error during teardown`); this.log(`Error during teardown`);

View File

@ -3,9 +3,9 @@ import { promises as fs } from "fs";
import process from "process"; import process from "process";
import { quote } from "shell-quote"; import { quote } from "shell-quote";
import { v4 as getUUID } from "uuid";
import { borrowUser } from "./users.js"; import { getUUID } from "./util.js";
import { import {
privilegedSetup, privilegedSetup,
privilegedSpawn, privilegedSpawn,
@ -29,9 +29,8 @@ async function main() {
die("environment variable unset: $L"); die("environment variable unset: $L");
} }
const uuid = getUUID(); const uuid = getUUID();
const { uid, returnUser } = await borrowUser(log); await run(privilegedSetup({ uuid }), log);
await run(privilegedSetup({ uid, uuid }), log); const args = privilegedSpawn({ uuid }, [
const args = privilegedSpawn({ uid, uuid }, [
"bash", "bash",
"-c", "-c",
`exec env L='${lang}' bash --rcfile <(cat <<< ${quote([sandboxScript])})`, `exec env L='${lang}' bash --rcfile <(cat <<< ${quote([sandboxScript])})`,
@ -43,7 +42,7 @@ async function main() {
proc.on("error", reject); proc.on("error", reject);
proc.on("close", resolve); proc.on("close", resolve);
}); });
await run(privilegedTeardown({ uid, uuid }), log); await run(privilegedTeardown({ uuid }), log);
await returnUser(); await returnUser();
} }

View File

@ -5,10 +5,10 @@ import _ from "lodash";
import pQueue from "p-queue"; import pQueue from "p-queue";
const PQueue = pQueue.default; const PQueue = pQueue.default;
import stripAnsi from "strip-ansi"; import stripAnsi from "strip-ansi";
import { v4 as getUUID } from "uuid";
import * as api from "./api.js"; import * as api from "./api.js";
import { langsPromise } from "./langs.js"; import { langsPromise } from "./langs.js";
import { getUUID } from "./util.js";
let langs = {}; let langs = {};

View File

@ -1,115 +0,0 @@
import { spawn } from "child_process";
import { promises as fs } from "fs";
import os from "os";
import AsyncLock from "async-lock";
import _ from "lodash";
import parsePasswd from "parse-passwd";
import { asBool, privilegedUseradd, run, uuidRegexp } from "./util.js";
// Keep in sync with system/src/riju-system-privileged.c
export const MIN_UID = 2000;
export const MAX_UID = 65000;
function validUID(uid) {
return uid >= MIN_UID && uid < MAX_UID;
}
const CUR_UID = os.userInfo().uid;
const ASSUME_SINGLE_PROCESS = asBool(
process.env.RIJU_ASSUME_SINGLE_PROCESS,
false
);
let initialized = false;
let nextUserToCreate = null;
let locallyBorrowedUsers = new Set();
let availableUsers = new Set();
let lock = new AsyncLock();
async function getCreatedUsers() {
return new Set(
parsePasswd(await fs.readFile("/etc/passwd", "utf-8"))
.map(({ uid }) => parseInt(uid))
.filter((uid) => !isNaN(uid) && validUID(uid))
);
}
async function getActiveUsers() {
let dirents;
try {
dirents = await fs.readdir("/tmp/riju");
} catch (err) {
if (err.code === "ENOENT") {
return new Set();
}
throw err;
}
return new Set(
(
await Promise.all(
dirents
.filter((name) => name.match(uuidRegexp))
.map((name) => fs.stat(`/tmp/riju/${name}`))
)
)
.map(({ uid }) => uid)
.filter(validUID)
);
}
async function createUser(log) {
if (nextUserToCreate >= MAX_UID) {
throw new Error("too many users");
}
const uid = nextUserToCreate;
await run(privilegedUseradd(uid), log);
nextUserToCreate += 1;
return uid;
}
export async function borrowUser(log) {
return await lock.acquire("key", async () => {
if (!initialized || !ASSUME_SINGLE_PROCESS) {
const createdUsers = await getCreatedUsers();
const activeUsers = await getActiveUsers();
if (createdUsers.size > 0) {
nextUserToCreate = _.max([...createdUsers]) + 1;
} else {
nextUserToCreate = MIN_UID;
}
// If there are new users created, we want to make them
// available (unless they are already active). Similarly, if
// there are users that have become inactive, we want to make
// them available (unless they are already borrowed locally).
for (const user of createdUsers) {
if (!activeUsers.has(user) && !locallyBorrowedUsers.has(user)) {
availableUsers.add(user);
}
}
// If there are users that have become active, we want to make
// them unavailable.
for (const user of activeUsers) {
availableUsers.delete(user);
}
initialized = true;
}
if (availableUsers.size === 0) {
availableUsers.add(await createUser(log));
}
// https://stackoverflow.com/a/32539929/3538165
const user = availableUsers.values().next().value;
locallyBorrowedUsers.add(user);
availableUsers.delete(user);
return {
uid: user,
returnUser: async () => {
await lock.acquire("key", () => {
locallyBorrowedUsers.delete(user);
availableUsers.add(user);
});
},
};
});
}

View File

@ -3,55 +3,12 @@ import os from "os";
import process from "process"; import process from "process";
import { quote } from "shell-quote"; import { quote } from "shell-quote";
import { v4 as getUUIDOrig } from "uuid";
import { MIN_UID, MAX_UID } from "./users.js";
export const rijuSystemPrivileged = "system/out/riju-system-privileged"; export const rijuSystemPrivileged = "system/out/riju-system-privileged";
const rubyVersion = (() => { export function getUUID() {
try { return getUUIDOrig().replace(/-/g, "");
return spawnSync("ruby", ["-e", "puts RUBY_VERSION"])
.stdout.toString()
.trim();
} catch (err) {
return null;
}
})();
function getEnv({ uid, uuid }) {
const cwd = `/tmp/riju/${uuid}`;
const path = [
rubyVersion && `${cwd}/.gem/ruby/${rubyVersion}/bin`,
`${cwd}/.local/bin`,
`${cwd}/node_modules/.bin`,
`/usr/local/sbin`,
`/usr/local/bin`,
`/usr/sbin`,
`/usr/bin`,
`/bin`,
].filter((x) => x);
const username =
uid >= MIN_UID && uid < MAX_UID ? `riju${uid}` : os.userInfo().username;
return {
HOME: cwd,
HOSTNAME: "riju",
LANG: "C.UTF-8",
LC_ALL: "C.UTF-8",
LOGNAME: username,
PATH: path.join(":"),
PWD: cwd,
SHELL: "/usr/bin/bash",
TERM: "xterm-256color",
TMPDIR: `${cwd}`,
USER: username,
USERNAME: username,
};
}
function getEnvString(ctx) {
return Object.entries(getEnv(ctx))
.map(([key, val]) => `${key}=${quote([val])}`)
.join(" ");
} }
export async function run(args, log, options) { export async function run(args, log, options) {
@ -87,30 +44,16 @@ export async function run(args, log, options) {
}); });
} }
export function privilegedUseradd(uid) { export function privilegedSession({ uuid, lang }) {
return [rijuSystemPrivileged, "useradd", `${uid}`]; return [rijuSystemPrivileged, "session", uuid, lang];
} }
export function privilegedSetup({ uid, uuid }) { export function privilegedWait({ uuid }) {
return [rijuSystemPrivileged, "setup", `${uid}`, uuid]; return [rijuSystemPrivileged, "wait", uuid];
} }
export function privilegedSpawn(ctx, args) { export function privilegedExec({ uuid }, args) {
const { uid, uuid } = ctx; return [rijuSystemPrivileged, "exec", uuid].concat(args);
return [
rijuSystemPrivileged,
"spawn",
`${uid}`,
uuid,
"sh",
"-c",
`exec env -i ${getEnvString(ctx)} "$@"`,
"--",
].concat(args);
}
export function privilegedTeardown({ uid, uuid }) {
return [rijuSystemPrivileged, "teardown", `${uid}`, uuid];
} }
export function bash(cmdline) { export function bash(cmdline) {
@ -130,9 +73,6 @@ export const log = {
error: console.error, error: console.error,
}; };
// https://gist.github.com/bugventure/f71337e3927c34132b9a
export const uuidRegexp = /^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$/;
export function asBool(value, def) { export function asBool(value, def) {
if (def === undefined) { if (def === undefined) {
throw new Error("asBool needs an explicit default value"); throw new Error("asBool needs an explicit default value");

View File

@ -22,17 +22,10 @@ COPY lib ./lib/
COPY backend ./backend/ COPY backend ./backend/
COPY langs ./langs/ COPY langs ./langs/
FROM ubuntu:rolling FROM riju:runtime
COPY docker/app/install.bash /tmp/
RUN /tmp/install.bash
COPY docker/shared/my_init /usr/local/sbin/
ENTRYPOINT ["/usr/local/sbin/my_init", "--quiet", "--"] ENTRYPOINT ["/usr/local/sbin/my_init", "--quiet", "--"]
RUN useradd -p '!' -m -l -s /usr/bin/bash riju RUN useradd -p '!' -m -l -s /usr/bin/bash riju
WORKDIR /src
COPY --chown=riju:riju --from=build /src ./ COPY --chown=riju:riju --from=build /src ./
RUN chown root:riju system/out/*-privileged && chmod a=,g=rx,u=rwxs system/out/*-privileged RUN chown root:riju system/out/*-privileged && chmod a=,g=rx,u=rwxs system/out/*-privileged

View File

@ -1,30 +0,0 @@
#!/usr/bin/env bash
set -euxo pipefail
export DEBIAN_FRONTEND=noninteractive
apt-get update
apt-get dist-upgrade -y
apt-get install -y curl gnupg lsb-release
curl -fsSL https://deb.nodesource.com/gpgkey/nodesource.gpg.key | apt-key add -
curl -fsSL https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add -
ubuntu_ver="$(lsb_release -rs)"
ubuntu_name="$(lsb_release -cs)"
node_repo="$(curl -sS https://deb.nodesource.com/setup_current.x | grep NODEREPO= | grep -Eo 'node_[0-9]+\.x' | head -n1)"
tee -a /etc/apt/sources.list.d/custom.list >/dev/null <<EOF
deb [arch=amd64] https://deb.nodesource.com/${node_repo} ${ubuntu_name} main
deb [arch=amd64] https://dl.yarnpkg.com/debian/ stable main
EOF
apt-get update
apt-get install -y make nodejs yarn
rm -rf /var/lib/apt/lists/*
rm "$0"

9
docker/base/Dockerfile Normal file
View File

@ -0,0 +1,9 @@
FROM ubuntu:rolling
COPY docker/base/install.bash /tmp/
RUN /tmp/install.bash
WORKDIR /src
COPY docker/shared/my_init /usr/local/sbin/
ENTRYPOINT ["/usr/local/sbin/my_init", "--quiet", "--"]
CMD ["bash"]

164
docker/base/install.bash Executable file
View File

@ -0,0 +1,164 @@
#!/usr/bin/env bash
set -euxo pipefail
latest_release() {
curl -sSL "https://api.github.com/repos/$1/releases/latest" | jq -r .tag_name
}
mkdir /tmp/riju-work
pushd /tmp/riju-work
export DEBIAN_FRONTEND=noninteractive
dpkg --add-architecture i386
apt-get update
(yes || true) | unminimize
apt-get install -y curl gnupg lsb-release wget
# Ceylon
wget https://cacerts.digicert.com/DigiCertTLSRSASHA2562020CA1.crt.pem -O /usr/local/share/ca-certificates/DigiCertTLSRSASHA2562020CA1.crt
# D
wget https://letsencrypt.org/certs/lets-encrypt-r3.pem -O /usr/local/share/ca-certificates/lets-encrypt-r3.crt
update-ca-certificates
ubuntu_ver="$(lsb_release -rs)"
ubuntu_name="$(lsb_release -cs)"
cran_repo="$(curl -fsSL https://cran.r-project.org/bin/linux/ubuntu/ | grep -Eo 'cran[0-9]+' | head -n1)"
node_repo="$(curl -fsSL https://deb.nodesource.com/setup_current.x | grep NODEREPO= | grep -Eo 'node_[0-9]+\.x' | head -n1)"
# .NET
wget "https://packages.microsoft.com/config/ubuntu/${ubuntu_ver}/packages-microsoft-prod.deb"
apt-get install ./packages-microsoft-prod.deb
# Ceylon
curl -fsSL https://downloads.ceylon-lang.org/apt/ceylon-debian-repo.gpg.key | apt-key add -
# Crystal
curl -fsSL https://keybase.io/crystal/pgp_keys.asc | apt-key add -
# Dart
curl -fsSL https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add -
# Hack
apt-key adv --keyserver keyserver.ubuntu.com --recv-keys B4112585D386EB94
# MongoDB
curl -fsSL https://www.mongodb.org/static/pgp/server-4.4.asc | apt-key add -
# Node.js
curl -fsSL https://deb.nodesource.com/gpgkey/nodesource.gpg.key | apt-key add -
# R
apt-key adv --keyserver keyserver.ubuntu.com --recv-keys E298A3A825C0D65DFD57CBB651716619E084DAB9
# Yarn
curl -fsSL https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add -
tee -a /etc/apt/sources.list.d/custom.list >/dev/null <<EOF
# Ceylon
deb [arch=amd64] https://downloads.ceylon-lang.org/apt/ unstable main
# Crystal
deb [arch=amd64] https://dist.crystal-lang.org/apt crystal main
# Dart
deb [arch=amd64] https://storage.googleapis.com/download.dartlang.org/linux/debian stable main
# Hack
deb [arch=amd64] https://dl.hhvm.com/ubuntu ${ubuntu_name} main
# MongoDB
deb [arch=amd64] https://repo.mongodb.org/apt/ubuntu focal/mongodb-org/4.4 multiverse
# Node.js
deb [arch=amd64] https://deb.nodesource.com/${node_repo} ${ubuntu_name} main
# R
deb [arch=amd64] https://cloud.r-project.org/bin/linux/ubuntu ${ubuntu_name}-${cran_repo}/
# Yarn
deb [arch=amd64] https://dl.yarnpkg.com/debian/ stable main
EOF
# Work around brutal packaging error courtesy of Microsoft.
# Unfortunately, the Microsoft repo includes a duplicate version of
# the libodbc1 package whose version is not in sync with the one
# shipped by the corresponding release of Ubuntu. If this one happens
# to be newer, then it can cause horrifyingly difficult to diagnose
# errors later on because there's a conflict between the
# default-available versions of libodbc1 and libodbc1:i386, which has
# in the past surfaced as an inability to install dependencies for
# Erlang. Thanks Microsoft. Please don't. Anyway, solution is to pin
# this repository at a lower priority than the Ubuntu standard
# packages, so the correct version of libodbc1 gets installed by
# default.
tee -a /etc/apt/preferences.d/riju >/dev/null <<EOF
Package: *
Pin: origin packages.microsoft.com
Pin-Priority: 1
EOF
apt-get update
apt-get install -y dctrl-tools
libicu="$(grep-aptavail -wF Package 'libicu[0-9]+' -s Package -n | head -n1)"
packages="
# compilation tools
clang
g++
gcc
make
# base languages
nodejs
ocaml
perl
python3
ruby
# packaging tools
apt-file
dctrl-tools
# basic utilities
bind9-dnsutils
less
git
htop
jq
make
man
moreutils
psmisc
ripgrep
strace
sudo
tmux
tree
vim
# shared dependencies
${libicu}
"
apt-get install -y $(sed 's/#.*//' <<< "${packages}")
rm -rf /var/lib/apt/lists/*
tee /etc/sudoers.d/90-riju >/dev/null <<"EOF"
%sudo ALL=(ALL:ALL) NOPASSWD: ALL
EOF
popd
rm -rf /tmp/riju-work
rm "$0"

View File

@ -1,4 +1,4 @@
FROM riju:runtime FROM riju:base
ARG LANG ARG LANG

View File

@ -2,8 +2,8 @@
set -euxo pipefail set -euxo pipefail
# See install.bash for the runtime image for much of the same, but # See install.bash for the base image for much of the same, but with
# with more comments. # more comments.
mkdir /tmp/riju-work mkdir /tmp/riju-work
pushd /tmp/riju-work pushd /tmp/riju-work

View File

@ -3,9 +3,10 @@ FROM ubuntu:rolling
COPY docker/runtime/install.bash /tmp/ COPY docker/runtime/install.bash /tmp/
RUN /tmp/install.bash RUN /tmp/install.bash
WORKDIR /src
COPY docker/shared/my_init docker/runtime/pid1.bash /usr/local/sbin/ COPY docker/shared/my_init docker/runtime/pid1.bash /usr/local/sbin/
ENTRYPOINT ["/usr/local/sbin/my_init", "--quiet", "--", "/usr/local/sbin/pid1.bash"] ENTRYPOINT ["/usr/local/sbin/my_init", "--quiet", "--", "/usr/local/sbin/pid1.bash"]
WORKDIR /src
CMD ["bash"] CMD ["bash"]
EXPOSE 6119 EXPOSE 6119
EXPOSE 6120 EXPOSE 6120

View File

@ -11,120 +11,32 @@ pushd /tmp/riju-work
export DEBIAN_FRONTEND=noninteractive export DEBIAN_FRONTEND=noninteractive
dpkg --add-architecture i386
apt-get update apt-get update
(yes || true) | unminimize (yes || true) | unminimize
apt-get install -y curl gnupg lsb-release wget apt-get install -y curl gnupg lsb-release wget
# Ceylon curl -fsSL https://deb.nodesource.com/gpgkey/nodesource.gpg.key | apt-key add -
wget https://cacerts.digicert.com/DigiCertTLSRSASHA2562020CA1.crt.pem -O /usr/local/share/ca-certificates/DigiCertTLSRSASHA2562020CA1.crt curl -fsSL https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add -
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | apt-key add -
# D
wget https://letsencrypt.org/certs/lets-encrypt-r3.pem -O /usr/local/share/ca-certificates/lets-encrypt-r3.crt
update-ca-certificates
ubuntu_ver="$(lsb_release -rs)"
ubuntu_name="$(lsb_release -cs)" ubuntu_name="$(lsb_release -cs)"
cran_repo="$(curl -fsSL https://cran.r-project.org/bin/linux/ubuntu/ | grep -Eo 'cran[0-9]+' | head -n1)" node_repo="$(curl -sS https://deb.nodesource.com/setup_current.x | grep NODEREPO= | grep -Eo 'node_[0-9]+\.x' | head -n1)"
node_repo="$(curl -fsSL https://deb.nodesource.com/setup_current.x | grep NODEREPO= | grep -Eo 'node_[0-9]+\.x' | head -n1)"
# .NET
wget "https://packages.microsoft.com/config/ubuntu/${ubuntu_ver}/packages-microsoft-prod.deb"
apt-get install ./packages-microsoft-prod.deb
# Ceylon
curl -fsSL https://downloads.ceylon-lang.org/apt/ceylon-debian-repo.gpg.key | apt-key add -
# Crystal
curl -fsSL https://keybase.io/crystal/pgp_keys.asc | apt-key add -
# Dart
curl -fsSL https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add -
# Hack
apt-key adv --keyserver keyserver.ubuntu.com --recv-keys B4112585D386EB94
# MongoDB
curl -fsSL https://www.mongodb.org/static/pgp/server-4.4.asc | apt-key add -
# Node.js
curl -fsSL https://deb.nodesource.com/gpgkey/nodesource.gpg.key | apt-key add -
# R
apt-key adv --keyserver keyserver.ubuntu.com --recv-keys E298A3A825C0D65DFD57CBB651716619E084DAB9
# Yarn
curl -fsSL https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add -
tee -a /etc/apt/sources.list.d/custom.list >/dev/null <<EOF tee -a /etc/apt/sources.list.d/custom.list >/dev/null <<EOF
# Ceylon
deb [arch=amd64] https://downloads.ceylon-lang.org/apt/ unstable main
# Crystal
deb [arch=amd64] https://dist.crystal-lang.org/apt crystal main
# Dart
deb [arch=amd64] https://storage.googleapis.com/download.dartlang.org/linux/debian stable main
# Hack
deb [arch=amd64] https://dl.hhvm.com/ubuntu ${ubuntu_name} main
# MongoDB
deb [arch=amd64] https://repo.mongodb.org/apt/ubuntu focal/mongodb-org/4.4 multiverse
# Node.js
deb [arch=amd64] https://deb.nodesource.com/${node_repo} ${ubuntu_name} main deb [arch=amd64] https://deb.nodesource.com/${node_repo} ${ubuntu_name} main
# R
deb [arch=amd64] https://cloud.r-project.org/bin/linux/ubuntu ${ubuntu_name}-${cran_repo}/
# Yarn
deb [arch=amd64] https://dl.yarnpkg.com/debian/ stable main deb [arch=amd64] https://dl.yarnpkg.com/debian/ stable main
deb [arch=amd64] https://download.docker.com/linux/ubuntu ${ubuntu_name} stable
EOF EOF
# Work around brutal packaging error courtesy of Microsoft.
# Unfortunately, the Microsoft repo includes a duplicate version of
# the libodbc1 package whose version is not in sync with the one
# shipped by the corresponding release of Ubuntu. If this one happens
# to be newer, then it can cause horrifyingly difficult to diagnose
# errors later on because there's a conflict between the
# default-available versions of libodbc1 and libodbc1:i386, which has
# in the past surfaced as an inability to install dependencies for
# Erlang. Thanks Microsoft. Please don't. Anyway, solution is to pin
# this repository at a lower priority than the Ubuntu standard
# packages, so the correct version of libodbc1 gets installed by
# default.
tee -a /etc/apt/preferences.d/riju >/dev/null <<EOF
Package: *
Pin: origin packages.microsoft.com
Pin-Priority: 1
EOF
apt-get update
apt-get install -y dctrl-tools
libicu="$(grep-aptavail -wF Package 'libicu[0-9]+' -s Package -n | head -n1)"
packages=" packages="
# compilation tools
clang
g++
gcc
make
# base languages
nodejs
ocaml
perl
python3
ruby
# project tools # project tools
clang
docker-ce-cli
make
nodejs
yarn yarn
# packaging tools # packaging tools
@ -148,11 +60,9 @@ tmux
tree tree
vim vim
# shared dependencies
${libicu}
" "
apt-get update
apt-get install -y $(sed 's/#.*//' <<< "${packages}") apt-get install -y $(sed 's/#.*//' <<< "${packages}")
ver="$(latest_release watchexec/watchexec)" ver="$(latest_release watchexec/watchexec)"
@ -166,9 +76,6 @@ tee /etc/sudoers.d/90-riju >/dev/null <<"EOF"
%sudo ALL=(ALL:ALL) NOPASSWD: ALL %sudo ALL=(ALL:ALL) NOPASSWD: ALL
EOF EOF
mkdir -p /opt/riju/langs
touch /opt/riju/langs/.keep
popd popd
rm -rf /tmp/riju-work rm -rf /tmp/riju-work

View File

@ -19,9 +19,7 @@ for src in system/src/*.c; do
out="${out/.c}" out="${out/.c}"
verbosely clang -Wall -Wextra -Werror -std=c11 "${src}" -o "${out}" verbosely clang -Wall -Wextra -Werror -std=c11 "${src}" -o "${out}"
if [[ "${out}" == *-privileged ]]; then if [[ "${out}" == *-privileged ]]; then
if getent group riju >/dev/null; then verbosely sudo chown root:riju "${out}"
sudo chown root:riju "${out}" verbosely sudo chmod a=,g=rx,u=rwxs "${out}"
fi
sudo chmod a=,g=rx,u=rwxs "${out}"
fi fi
done done

View File

@ -1,19 +1,15 @@
#define _GNU_SOURCE #define _GNU_SOURCE
#include <errno.h> #include <errno.h>
#include <grp.h> #include <grp.h>
#include <signal.h>
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
#include <sys/stat.h> #include <sys/stat.h>
#include <sys/types.h> #include <sys/types.h>
#include <time.h>
#include <unistd.h> #include <unistd.h>
// Keep in sync with backend/src/users.ts
const int MIN_UID = 2000;
const int MAX_UID = 65000;
int privileged;
void __attribute__ ((noreturn)) die(char *msg) void __attribute__ ((noreturn)) die(char *msg)
{ {
fprintf(stderr, "%s\n", msg); fprintf(stderr, "%s\n", msg);
@ -23,155 +19,136 @@ void __attribute__ ((noreturn)) die(char *msg)
void die_with_usage() void die_with_usage()
{ {
die("usage:\n" die("usage:\n"
" riju-system-privileged useradd UID\n" " riju-system-privileged session UUID LANG\n"
" riju-system-privileged setup UID UUID\n" " riju-system-privileged wait UUID\n"
" riju-system-privileged spawn UID UUID CMDLINE...\n" " riju-system-privileged exec UUID CMDLINE...");
" riju-system-privileged teardown UID UUID");
}
int parseUID(char *str)
{
if (!privileged)
return -1;
char *endptr;
long uid = strtol(str, &endptr, 10);
if (!*str || *endptr)
die("uid must be an integer");
if (uid < MIN_UID || uid >= MAX_UID)
die("uid is out of range");
return uid;
} }
char *parseUUID(char *uuid) char *parseUUID(char *uuid)
{ {
if (!*uuid) if (strnlen(uuid, 33) != 32)
die("illegal uuid"); die("illegal uuid");
for (char *ptr = uuid; *ptr; ++ptr) for (char *ptr = uuid; *ptr; ++ptr)
if (!((*ptr >= 'a' && *ptr <= 'z') || (*ptr >= '0' && *ptr <= '9') || *ptr == '-')) if (!((*ptr >= 'a' && *ptr <= 'z') || (*ptr >= '0' && *ptr <= '9')))
die("illegal uuid"); die("illegal uuid");
return uuid; return uuid;
} }
void useradd(int uid) char *parseLang(char *lang) {
{ size_t len = strnlen(lang, 65);
if (!privileged) if (len == 0 || len > 64)
die("useradd not allowed without root privileges"); die("illegal language name");
char *cmdline; return lang;
if (asprintf(&cmdline, "groupadd -g %1$d riju%1$d", uid) < 0)
die("asprintf failed");
int status = system(cmdline);
if (status != 0)
die("groupadd failed");
if (asprintf(&cmdline, "useradd -M -N -l -r -u %1$d -g %1$d -p '!' -s /usr/bin/bash riju%1$d", uid) < 0)
die("asprintf failed");
status = system(cmdline);
if (status != 0)
die("useradd failed");
} }
void spawn(int uid, char *uuid, char **cmdline) void session(char *uuid, char *lang)
{ {
char *cwd; char *image, *container;
if (asprintf(&cwd, "/tmp/riju/%s", uuid) < 0) if (asprintf(&image, "riju:lang-%s", lang) < 0)
die("asprintf failed"); die("asprintf failed");
if (chdir(cwd) < 0) if (asprintf(&container, "riju-session-%s", uuid) < 0)
die("chdir failed"); die("asprintf failed");
if (privileged) { char *argv[] = {
if (setgid(uid) < 0) "docker",
die("setgid failed"); "run",
if (setgroups(0, NULL) < 0) "--rm",
die("setgroups failed"); "-e", "HOME=/home/riju",
if (setuid(uid) < 0) "-e", "HOSTNAME=riju",
die("setuid failed"); "-e", "LANG=C.UTF-8",
} "-e", "LC_ALL=C.UTF-8",
umask(077); "-e", "LOGNAME=riju",
execvp(cmdline[0], cmdline); "-e", "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/bin",
"-e", "PWD=/home/riju/src",
"-e", "SHELL=/usr/bin/bash",
"-e", "TERM=xterm-256color",
"-e", "TMPDIR=/tmp",
"-e", "USER=riju",
"-e", "USERNAME=riju",
"--hostname", "riju",
"--name", container,
image, "tail", "-f", "/dev/null", NULL,
};
execvp(argv[0], argv);
die("execvp failed"); die("execvp failed");
} }
void setup(int uid, char *uuid) void wait_alarm(int signum)
{ {
char *cmdline; (void)signum;
if (asprintf(&cmdline, privileged die("container did not come up within 1 second");
? "install -d -o riju%1$d -g riju%1$d -m 700 /tmp/riju/%2$s"
: "install -d -m 700 /tmp/riju/%2$s", uid, uuid) < 0)
die("asprintf failed");
int status = system(cmdline);
if (status != 0)
die("install failed");
} }
void teardown(int uid, char *uuid) void wait(char *uuid)
{ {
char *cmdline; char *cmdline;
int status; if (asprintf(&cmdline, "docker inspect riju-session-%s", uuid) < 0)
char *users;
if (uid >= MIN_UID && uid < MAX_UID) {
if (asprintf(&users, "%d", uid) < 0)
die("asprintf failed");
} else {
cmdline = "getent passwd | grep -Eo '^riju[0-9]{4}' | paste -s -d, - | tr -d '\n'";
FILE *fp = popen(cmdline, "r");
if (fp == NULL)
die("popen failed");
static char buf[(MAX_UID - MIN_UID) * 9];
if (fgets(buf, sizeof(buf), fp) == NULL) {
if (feof(fp))
users = NULL;
else {
die("fgets failed");
}
} else
users = buf;
}
if (users != NULL) {
if (asprintf(&cmdline, "while pkill -9 --uid %1$s; do sleep 0.01; done", users) < 0)
die("asprintf failed");
status = system(cmdline);
if (status != 0 && status != 256)
die("pkill failed");
}
if (asprintf(&cmdline, "rm -rf /tmp/riju/%s", uuid) < 0)
die("asprintf failed"); die("asprintf failed");
status = system(cmdline); struct timespec ts;
if (status != 0) ts.tv_sec = 0;
die("rm failed"); ts.tv_nsec = 1000 * 1000 * 10;
signal(SIGALRM, wait_alarm);
alarm(1);
while (1) {
FILE *proc = popen(cmdline, "r");
if (proc == NULL)
die("popen failed");
int status = pclose(proc);
if (status < 0)
die("pclose failed");
if (WEXITSTATUS(status) == 0)
break;
int rv = nanosleep(&ts, NULL);
if (rv != 0 && rv != EINTR)
die("nanosleep failed");
}
}
void exec(char *uuid, int argc, char **cmdline)
{
char *container;
if (asprintf(&container, "riju-session-%s", uuid) < 0)
die("asprintf failed");
char *argvPrefix[] = {
"docker",
"exec",
"-it",
container,
};
char **argv = malloc(sizeof(argvPrefix) + (argc + 1) * sizeof(char *));
if (argv == NULL)
die("malloc failed");
memcpy(argv, argvPrefix, sizeof(argvPrefix));
memcpy((void *)argv + sizeof(argvPrefix), cmdline, argc * sizeof(char *));
argv[sizeof(argvPrefix) + argc * sizeof(char *)] = NULL;
execvp(argv[0], argv);
die("execvp failed");
} }
int main(int argc, char **argv) int main(int argc, char **argv)
{ {
int code = setuid(0); if (setuid(0) != 0)
if (code != 0 && code != -EPERM)
die("setuid failed"); die("setuid failed");
privileged = code == 0;
if (argc < 2) if (argc < 2)
die_with_usage(); die_with_usage();
if (!strcmp(argv[1], "useradd")) { if (!strcmp(argv[1], "session")) {
if (argc != 4)
die_with_usage();
char *uuid = parseUUID(argv[2]);
char *lang = parseLang(argv[3]);
session(uuid, lang);
return 0;
}
if (!strcmp(argv[1], "wait")) {
if (argc != 3) if (argc != 3)
die_with_usage(); die_with_usage();
useradd(parseUID(argv[2])); char *uuid = parseUUID(argv[2]);
wait(uuid);
return 0; return 0;
} }
if (!strcmp(argv[1], "spawn")) { if (!strcmp(argv[1], "exec")) {
if (argc < 5) if (argc < 4)
die_with_usage(); die_with_usage();
spawn(parseUID(argv[2]), parseUUID(argv[3]), &argv[4]); exec(parseUUID(argv[2]), argc, &argv[3]);
return 0;
}
if (!strcmp(argv[1], "setup")) {
if (argc != 4)
die_with_usage();
int uid = parseUID(argv[2]);
char *uuid = parseUUID(argv[3]);
setup(uid, uuid);
return 0;
}
if (!strcmp(argv[1], "teardown")) {
if (argc != 4)
die_with_usage();
int uid = strcmp(argv[2], "*") ? parseUID(argv[2]) : -1;
char *uuid = strcmp(argv[3], "*") ? parseUUID(argv[3]) : "*";
teardown(uid, uuid);
return 0; return 0;
} }
die_with_usage(); die_with_usage();

View File

@ -28,10 +28,7 @@ async function main() {
const hash = await hashDockerfile( const hash = await hashDockerfile(
"lang", "lang",
{ {
"riju:runtime": await getLocalImageLabel( "riju:base": await getLocalImageLabel("riju:base", "riju.image-hash"),
"riju:runtime",
"riju.image-hash"
),
}, },
{ {
salt: { salt: {
@ -52,7 +49,7 @@ async function main() {
try { try {
if (debug) { if (debug) {
await runCommand( await runCommand(
`docker run -it --rm -e LANG=${lang} -w /tmp/riju-work --network host riju:runtime` `docker run -it --rm -e LANG=${lang} -w /tmp/riju-work --network host base:runtime`
); );
} else { } else {
await runCommand( await runCommand(

View File

@ -12,7 +12,7 @@ import _ from "lodash";
import { getLocalImageDigest, getLocalImageLabel } from "./docker-util.js"; import { getLocalImageDigest, getLocalImageLabel } from "./docker-util.js";
import { runCommand } from "./util.js"; import { runCommand } from "./util.js";
// Given a string like "runtime" that identifies the relevant // Given a string like "base" that identifies the relevant
// Dockerfile, read it from disk and parse it into a list of commands. // Dockerfile, read it from disk and parse it into a list of commands.
async function parseDockerfile(name) { async function parseDockerfile(name) {
const contents = await fs.readFile(`docker/${name}/Dockerfile`, "utf-8"); const contents = await fs.readFile(`docker/${name}/Dockerfile`, "utf-8");

View File

@ -149,9 +149,7 @@ async function planDebianPackages(opts) {
} }
clauses.push(`make installs L=${lang}`); clauses.push(`make installs L=${lang}`);
clauses.push("make test"); clauses.push("make test");
await runCommand( await runCommand(`make shell I=base CMD="${clauses.join(" && ")}"`);
`make shell I=runtime CMD="${clauses.join(" && ")}"`
);
} }
await runCommand(`make upload L=${lang} T=${type}`); await runCommand(`make upload L=${lang} T=${type}`);
}, },
@ -186,12 +184,12 @@ async function computePlan() {
"ubuntu:rolling": await getLocalImageDigest("ubuntu:rolling"), "ubuntu:rolling": await getLocalImageDigest("ubuntu:rolling"),
}; };
const packaging = await planDockerImage("packaging", dependentHashes); const packaging = await planDockerImage("packaging", dependentHashes);
const runtime = await planDockerImage("runtime", dependentHashes); const base = await planDockerImage("base", dependentHashes);
const packages = await planDebianPackages({ const packages = await planDebianPackages({
deps: [packaging.id, runtime.id], deps: [packaging.id, base.id],
}); });
const composite = await planDockerImage("composite", dependentHashes, { const composite = await planDockerImage("composite", dependentHashes, {
deps: [runtime.id, ...packages.map(({ id }) => id)], deps: [base.id, ...packages.map(({ id }) => id)],
hashOpts: { hashOpts: {
salt: { salt: {
packageHashes: packages.map(({ desired }) => desired).sort(), packageHashes: packages.map(({ desired }) => desired).sort(),
@ -202,7 +200,7 @@ async function computePlan() {
const app = await planDockerImage("app", dependentHashes, { const app = await planDockerImage("app", dependentHashes, {
deps: [composite.id, compile.id], deps: [composite.id, compile.id],
}); });
return [packaging, runtime, ...packages, composite, compile, app]; return [packaging, base, ...packages, composite, compile, app];
} }
function printTable(data, headers) { function printTable(data, headers) {