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)
else ifeq ($(I),app)
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)
@: $${L}
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)
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
docker run -it --rm --hostname $(I) -v $(VOLUME_MOUNT):/src $(SHELL_PORTS) $(SHELL_ENV) riju:$(I) $(BASH_CMD)
endif

View File

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

View File

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

View File

@ -5,10 +5,10 @@ import _ from "lodash";
import pQueue from "p-queue";
const PQueue = pQueue.default;
import stripAnsi from "strip-ansi";
import { v4 as getUUID } from "uuid";
import * as api from "./api.js";
import { langsPromise } from "./langs.js";
import { getUUID } from "./util.js";
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 { quote } from "shell-quote";
import { MIN_UID, MAX_UID } from "./users.js";
import { v4 as getUUIDOrig } from "uuid";
export const rijuSystemPrivileged = "system/out/riju-system-privileged";
const rubyVersion = (() => {
try {
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 function getUUID() {
return getUUIDOrig().replace(/-/g, "");
}
export async function run(args, log, options) {
@ -87,30 +44,16 @@ export async function run(args, log, options) {
});
}
export function privilegedUseradd(uid) {
return [rijuSystemPrivileged, "useradd", `${uid}`];
export function privilegedSession({ uuid, lang }) {
return [rijuSystemPrivileged, "session", uuid, lang];
}
export function privilegedSetup({ uid, uuid }) {
return [rijuSystemPrivileged, "setup", `${uid}`, uuid];
export function privilegedWait({ uuid }) {
return [rijuSystemPrivileged, "wait", uuid];
}
export function privilegedSpawn(ctx, args) {
const { uid, uuid } = ctx;
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 privilegedExec({ uuid }, args) {
return [rijuSystemPrivileged, "exec", uuid].concat(args);
}
export function bash(cmdline) {
@ -130,9 +73,6 @@ export const log = {
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) {
if (def === undefined) {
throw new Error("asBool needs an explicit default value");

View File

@ -22,17 +22,10 @@ COPY lib ./lib/
COPY backend ./backend/
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", "--"]
RUN useradd -p '!' -m -l -s /usr/bin/bash riju
WORKDIR /src
COPY --chown=riju:riju --from=build /src ./
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

View File

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

View File

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

View File

@ -11,120 +11,32 @@ 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
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 -
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)"
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 -
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
# 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
deb [arch=amd64] https://download.docker.com/linux/ubuntu ${ubuntu_name} stable
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
# project tools
clang
docker-ce-cli
make
nodejs
yarn
# packaging tools
@ -148,11 +60,9 @@ tmux
tree
vim
# shared dependencies
${libicu}
"
apt-get update
apt-get install -y $(sed 's/#.*//' <<< "${packages}")
ver="$(latest_release watchexec/watchexec)"
@ -166,9 +76,6 @@ tee /etc/sudoers.d/90-riju >/dev/null <<"EOF"
%sudo ALL=(ALL:ALL) NOPASSWD: ALL
EOF
mkdir -p /opt/riju/langs
touch /opt/riju/langs/.keep
popd
rm -rf /tmp/riju-work

View File

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

View File

@ -1,19 +1,15 @@
#define _GNU_SOURCE
#include <errno.h>
#include <grp.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <time.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)
{
fprintf(stderr, "%s\n", msg);
@ -23,155 +19,136 @@ void __attribute__ ((noreturn)) die(char *msg)
void die_with_usage()
{
die("usage:\n"
" riju-system-privileged useradd UID\n"
" riju-system-privileged setup UID UUID\n"
" riju-system-privileged spawn UID UUID CMDLINE...\n"
" 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;
" riju-system-privileged session UUID LANG\n"
" riju-system-privileged wait UUID\n"
" riju-system-privileged exec UUID CMDLINE...");
}
char *parseUUID(char *uuid)
{
if (!*uuid)
if (strnlen(uuid, 33) != 32)
die("illegal uuid");
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");
return uuid;
}
void useradd(int uid)
{
if (!privileged)
die("useradd not allowed without root privileges");
char *cmdline;
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");
char *parseLang(char *lang) {
size_t len = strnlen(lang, 65);
if (len == 0 || len > 64)
die("illegal language name");
return lang;
}
void spawn(int uid, char *uuid, char **cmdline)
void session(char *uuid, char *lang)
{
char *cwd;
if (asprintf(&cwd, "/tmp/riju/%s", uuid) < 0)
char *image, *container;
if (asprintf(&image, "riju:lang-%s", lang) < 0)
die("asprintf failed");
if (chdir(cwd) < 0)
die("chdir failed");
if (privileged) {
if (setgid(uid) < 0)
die("setgid failed");
if (setgroups(0, NULL) < 0)
die("setgroups failed");
if (setuid(uid) < 0)
die("setuid failed");
}
umask(077);
execvp(cmdline[0], cmdline);
if (asprintf(&container, "riju-session-%s", uuid) < 0)
die("asprintf failed");
char *argv[] = {
"docker",
"run",
"--rm",
"-e", "HOME=/home/riju",
"-e", "HOSTNAME=riju",
"-e", "LANG=C.UTF-8",
"-e", "LC_ALL=C.UTF-8",
"-e", "LOGNAME=riju",
"-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");
}
void setup(int uid, char *uuid)
void wait_alarm(int signum)
{
char *cmdline;
if (asprintf(&cmdline, privileged
? "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)signum;
die("container did not come up within 1 second");
}
void teardown(int uid, char *uuid)
void wait(char *uuid)
{
char *cmdline;
int status;
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)
if (asprintf(&cmdline, "docker inspect riju-session-%s", uuid) < 0)
die("asprintf failed");
status = system(cmdline);
if (status != 0)
die("rm failed");
struct timespec ts;
ts.tv_sec = 0;
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 code = setuid(0);
if (code != 0 && code != -EPERM)
if (setuid(0) != 0)
die("setuid failed");
privileged = code == 0;
if (argc < 2)
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)
die_with_usage();
useradd(parseUID(argv[2]));
char *uuid = parseUUID(argv[2]);
wait(uuid);
return 0;
}
if (!strcmp(argv[1], "spawn")) {
if (argc < 5)
if (!strcmp(argv[1], "exec")) {
if (argc < 4)
die_with_usage();
spawn(parseUID(argv[2]), parseUUID(argv[3]), &argv[4]);
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);
exec(parseUUID(argv[2]), argc, &argv[3]);
return 0;
}
die_with_usage();

View File

@ -28,10 +28,7 @@ async function main() {
const hash = await hashDockerfile(
"lang",
{
"riju:runtime": await getLocalImageLabel(
"riju:runtime",
"riju.image-hash"
),
"riju:base": await getLocalImageLabel("riju:base", "riju.image-hash"),
},
{
salt: {
@ -52,7 +49,7 @@ async function main() {
try {
if (debug) {
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 {
await runCommand(

View File

@ -12,7 +12,7 @@ import _ from "lodash";
import { getLocalImageDigest, getLocalImageLabel } from "./docker-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.
async function parseDockerfile(name) {
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 test");
await runCommand(
`make shell I=runtime CMD="${clauses.join(" && ")}"`
);
await runCommand(`make shell I=base CMD="${clauses.join(" && ")}"`);
}
await runCommand(`make upload L=${lang} T=${type}`);
},
@ -186,12 +184,12 @@ async function computePlan() {
"ubuntu:rolling": await getLocalImageDigest("ubuntu:rolling"),
};
const packaging = await planDockerImage("packaging", dependentHashes);
const runtime = await planDockerImage("runtime", dependentHashes);
const base = await planDockerImage("base", dependentHashes);
const packages = await planDebianPackages({
deps: [packaging.id, runtime.id],
deps: [packaging.id, base.id],
});
const composite = await planDockerImage("composite", dependentHashes, {
deps: [runtime.id, ...packages.map(({ id }) => id)],
deps: [base.id, ...packages.map(({ id }) => id)],
hashOpts: {
salt: {
packageHashes: packages.map(({ desired }) => desired).sort(),
@ -202,7 +200,7 @@ async function computePlan() {
const app = await planDockerImage("app", dependentHashes, {
deps: [composite.id, compile.id],
});
return [packaging, runtime, ...packages, composite, compile, app];
return [packaging, base, ...packages, composite, compile, app];
}
function printTable(data, headers) {