Preliminary containerization work
This commit is contained in:
		
							parent
							
								
									83208355d4
								
							
						
					
					
						commit
						b99d17bcd3
					
				
							
								
								
									
										4
									
								
								Makefile
								
								
								
								
							
							
						
						
									
										4
									
								
								Makefile
								
								
								
								
							| 
						 | 
				
			
			@ -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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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`);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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 = {};
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										115
									
								
								backend/users.js
								
								
								
								
							
							
						
						
									
										115
									
								
								backend/users.js
								
								
								
								
							| 
						 | 
				
			
			@ -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);
 | 
			
		||||
        });
 | 
			
		||||
      },
 | 
			
		||||
    };
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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");
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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"
 | 
			
		||||
| 
						 | 
				
			
			@ -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"]
 | 
			
		||||
| 
						 | 
				
			
			@ -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"
 | 
			
		||||
| 
						 | 
				
			
			@ -1,4 +1,4 @@
 | 
			
		|||
FROM riju:runtime
 | 
			
		||||
FROM riju:base
 | 
			
		||||
 | 
			
		||||
ARG LANG
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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();
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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(
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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");
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue