-
Notifications
You must be signed in to change notification settings - Fork 13
Update Dockerfile #350
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
CristianCantoro
wants to merge
3
commits into
olimpiadi-informatica:master
Choose a base branch
from
CristianCantoro:master
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Update Dockerfile #350
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,47 +1,96 @@ | ||
| FROM ubuntu:18.04 | ||
| LABEL maintainer="Edoardo Morassutto <[email protected]>" | ||
| FROM ubuntu:24.04 | ||
|
|
||
| ARG UID=1000 | ||
| ARG GID=1000 | ||
| # Example invocation of docker build | ||
| # $ docker build \ | ||
| # --build-arg TM_VERSION=X.Y.Z \ | ||
| # --build-arg VCS_REF="$(git rev-parse HEAD)" \ | ||
| # --build-arg BUILD_DATE="$(date -u +%Y-%m-%dT%H:%M:%SZ)" \ | ||
| # -t <yourrepo>/task-maker-rust:X.Y.Z | ||
| # . | ||
|
|
||
| ARG TM_VERSION # task-maker-rust version (required) | ||
| ARG VCS_REF # git commit SHA for v${TM_VERSION} | ||
| ARG BUILD_DATE # e.g. 2025-09-04T12:34:56Z (RFC 3339 UTC) | ||
| ARG IMAGE_URL | ||
| ARG DOCS_URL='https://github.com/olimpiadi-informatica/task-maker-rust#readme' | ||
| ARG VENDOR='Olimpiadi Italiane di Informatica' | ||
| ARG BASE_NAME='ubuntu:24.04' | ||
| ARG BASE_DIGEST='sha256:9cbed754112939e914291337b5e554b07ad7c392491dba6daf25eef1332a22e8' | ||
|
|
||
| LABEL \ | ||
| org.opencontainers.image.title="task-maker-rust" \ | ||
| org.opencontainers.image.description="task-maker-rust server and worker to build programming tasks for CMS." \ | ||
| org.opencontainers.image.version="${TM_VERSION}" \ | ||
| org.opencontainers.image.revision="${VCS_REF}" \ | ||
| org.opencontainers.image.created="${BUILD_DATE}" \ | ||
| org.opencontainers.image.url="${IMAGE_URL}" \ | ||
| org.opencontainers.image.documentation="${DOCS_URL}" \ | ||
| org.opencontainers.image.source="https://github.com/olimpiadi-informatica/task-maker-rust" \ | ||
| org.opencontainers.image.authors="task-maker-rust contributors" \ | ||
| org.opencontainers.image.vendor="${VENDOR}" \ | ||
| org.opencontainers.image.licenses="MPL-2.0" \ | ||
| org.opencontainers.image.base.name="${BASE_NAME}" \ | ||
| org.opencontainers.image.base.digest="${BASE_DIGEST}" | ||
|
|
||
| ARG TM_UID=1000 | ||
| ARG TM_GID=1000 | ||
|
|
||
| ENV RUST_LOG='info' | ||
| ENV RUST_BACKTRACE=1 | ||
|
|
||
| # run the following as root | ||
| USER root | ||
|
|
||
| # install dependencies | ||
| RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -yy \ | ||
| RUN apt-get update \ | ||
| && DEBIAN_FRONTEND=noninteractive apt-get install -yy \ | ||
| asymptote \ | ||
| build-essential \ | ||
| fpc \ | ||
| latexmk \ | ||
| libseccomp-dev \ | ||
| python \ | ||
| python-sortedcontainers \ | ||
| libssl-dev \ | ||
| python3 \ | ||
| python3-sortedcontainers \ | ||
| texlive \ | ||
| texlive-latex-extra \ | ||
| wget \ | ||
| curl \ | ||
| && rm -rf /var/lib/apt/lists/* | ||
|
|
||
| # task-maker-rust version (required) | ||
| ARG TM_VERSION | ||
| # delete default ubuntu user, this removes also the group | ||
| RUN userdel --remove ubuntu | ||
|
|
||
| # install task-maker-rust | ||
| RUN (test -n "$TM_VERSION" || (echo "Please use --build-arg TM_VERSION=0.3.X" >&2 && exit 1)) \ | ||
| && wget https://github.com/olimpiadi-informatica/task-maker-rust/releases/download/v${TM_VERSION}/task-maker-rust_${TM_VERSION}_amd64.deb \ | ||
| && dpkg -i task-maker-rust_${TM_VERSION}_amd64.deb \ | ||
| && rm task-maker-rust_${TM_VERSION}_amd64.deb | ||
| # create a group and a user called taskmaker, create the home directory | ||
| RUN groupadd ${TM_GID:+-g "${TM_GID}"} task-maker | ||
| RUN useradd -m -g task-maker ${TM_UID:+-u "${TM_UID}"} task-maker | ||
|
|
||
| # drop root privileges | ||
| RUN groupadd -g $GID user \ | ||
| && useradd -m -g $GID -u $UID user | ||
| USER user | ||
| # use /home/task-maker as work directory | ||
| WORKDIR /home/task-maker | ||
|
|
||
| # install task-maker-rust using .deb from releases | ||
| ARG TM_DEB_VERSION="${TM_VERSION}-1.ubuntu-24.04" | ||
| ARG TM_DEB_NAME="task-maker-rust_${TM_DEB_VERSION}_amd64.deb" | ||
|
|
||
| RUN (test -n "$TM_VERSION" || (echo "Please use --build-arg TM_VERSION=X.Y.Z" >&2 && exit 1)) \ | ||
| && wget "https://github.com/olimpiadi-informatica/task-maker-rust/releases/download/v${TM_VERSION}/${TM_DEB_NAME}" \ | ||
| && dpkg -i "${TM_DEB_NAME}" \ | ||
| && rm -rf "${TM_DEB_NAME}" | ||
|
|
||
| # run everything as a unprivileged user | ||
| # (docker still needs --privileged to run because rask-maker-rust needs privileges to create a sandbox) | ||
| USER task-maker | ||
|
|
||
| # server-client port | ||
| EXPOSE 27182 | ||
| # server-worker port | ||
| EXPOSE 27183 | ||
|
|
||
| # start task-maker-rust server and worker | ||
| ADD entrypoint.sh healthcheck.sh / | ||
| CMD /entrypoint.sh | ||
| ADD entrypoint.py healthcheck.sh /home/task-maker | ||
|
|
||
| ENTRYPOINT ["/home/task-maker/entrypoint.py"] | ||
|
|
||
| # check the status of the server and the workers | ||
| HEALTHCHECK --interval=5s CMD /healthcheck.sh | ||
| HEALTHCHECK --interval=5s \ | ||
| CMD /home/task-maker/healthcheck.sh || exit 1 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,251 @@ | ||
| #!/usr/bin/env python3 | ||
| # -*- coding: utf-8 -*- | ||
|
|
||
| import os | ||
| import sys | ||
| import shlex | ||
| import shutil | ||
| import signal | ||
| import atexit | ||
| import time | ||
| import threading | ||
| import subprocess | ||
| import logging | ||
| from glob import glob | ||
| from pathlib import Path | ||
| from multiprocessing import cpu_count | ||
| import tempfile | ||
| import argparse | ||
| from typing import List, Optional, Any | ||
| from types import FrameType | ||
|
|
||
| # ---------- defaults from env ---------- | ||
| SERVER_ARGS: str = os.environ.get("SERVER_ARGS", "") | ||
| WORKER_ARGS: str = os.environ.get("WORKER_ARGS", "") | ||
| SERVER_ADDR: str = os.environ.get("SERVER_ADDR", "127.0.0.1:27183") | ||
| SPAWN_SERVER: bool = os.environ.get("SPAWN_SERVER", "true").lower() == "true" | ||
| SPAWN_WORKERS: bool = os.environ.get("SPAWN_WORKERS", "true").lower() == "true" | ||
| TM_LOGLEVEL: str = os.environ.get("TM_LOGLEVEL", "info").lower() | ||
|
|
||
| # default: nproc - 1 (at least 1) | ||
| default_nworkers: int = max(cpu_count() - 1, 1) | ||
|
|
||
| # ---------- logging ---------- | ||
| LOGGER_NAME = "tm_entrypoint" | ||
| logger = logging.getLogger(LOGGER_NAME) | ||
|
|
||
| def _map_tm_loglevel(level: str) -> int: | ||
| table = { | ||
| "error": logging.ERROR, | ||
| "warn": logging.WARNING, | ||
| "warning": logging.WARNING, | ||
| "info": logging.INFO, | ||
| "debug": logging.DEBUG, | ||
| } | ||
| return table.get(level, logging.ERROR) | ||
|
|
||
| def configure_logging() -> None: | ||
| logging.basicConfig( | ||
| level=_map_tm_loglevel(TM_LOGLEVEL), | ||
| format="[%(asctime)s %(levelname)s\t%(name)s::%(funcName)s] %(message)s", | ||
| datefmt= "%Y-%m-%dT%H:%M:%S.xxxxxxxxxZ", | ||
| stream=sys.stderr, | ||
| ) | ||
| eff = logging.getLevelName(logger.getEffectiveLevel()) | ||
| logger.debug("Logger initialized at DEBUG (effective: %s).", eff) | ||
| logger.info("Logging configured (level=%s).", eff) | ||
|
|
||
| # ---------- CLI ---------- | ||
| def cli() -> argparse.Namespace: | ||
| parser = argparse.ArgumentParser( | ||
| description="Entrypoint for task-maker-rust Docker image." | ||
| ) | ||
| parser.add_argument( | ||
| "-j", "--jobs", | ||
| type=int, | ||
| default=default_nworkers, | ||
| help=f"Number of workers to launch [default: <nproc>-1 = {default_nworkers}]" | ||
| ) | ||
|
|
||
| args = parser.parse_args() | ||
| assert args.jobs > 0, f"Please specify a positive number of jobs, not {args.jobs}." | ||
|
|
||
| return args | ||
|
|
||
| # ---------- cleanup ---------- | ||
| if SPAWN_SERVER or SPAWN_WORKERS: | ||
| def cleanup() -> None: | ||
| logger.debug("Cleaning up temporary stores") | ||
| for p in glob("/tmp/tmserver.*"): | ||
| logger.debug("Removing server store: %s", p) | ||
| shutil.rmtree(p, ignore_errors=True) | ||
| for p in glob("/tmp/tmworker.*"): | ||
| logger.debug("Removing worker store: %s", p) | ||
| shutil.rmtree(p, ignore_errors=True) | ||
| logger.debug("Cleanup complete.") | ||
|
|
||
| atexit.register(cleanup) | ||
|
|
||
| def _signal_handler(signum: int, frame: Optional[FrameType]) -> None: | ||
| name = signal.Signals(signum).name if hasattr(signal, "Signals") else str(signum) | ||
| logger.warning("Received signal %s — shutting down.", name) | ||
| # atexit will run cleanup | ||
| code = 130 if signum == signal.SIGINT else 143 | ||
| sys.exit(code) | ||
|
|
||
| signal.signal(signal.SIGINT, _signal_handler) | ||
| signal.signal(signal.SIGTERM, _signal_handler) | ||
|
|
||
| # ---------- verbosity mapping for task-maker-tools ---------- | ||
| def loglevel_verbosity_flag(level: str) -> str: | ||
| flag = { | ||
| "error": "", | ||
| "warn": "-v", | ||
| "warning": "-v", | ||
| "info": "-vv", | ||
| "debug": "-vvv", | ||
| }.get(level, "") | ||
| logger.debug(f"Mapped TM_LOGLEVEL={level} to " | ||
| f"task-maker-tools flag '{flag or "<none>"}'") | ||
| return flag | ||
|
|
||
| # ---------- spawn helpers ---------- | ||
| def make_worker_stores(nworkers: int) -> List[str]: | ||
| base = tempfile.mktemp(prefix="tmworker.", dir="/tmp") | ||
| stores: List[str] = [] | ||
| for i in range(1, nworkers + 1): | ||
| idx = f"{i:02d}" | ||
| d = f"{base}-{idx}" | ||
| Path(d).mkdir(parents=True, exist_ok=True) | ||
| stores.append(d) | ||
| logger.debug("Created %d worker stores (base=%s): %s", nworkers, base, stores) | ||
| return stores | ||
|
|
||
| def spawn_tmserver() -> int: | ||
| verbosity_flag: str = loglevel_verbosity_flag(TM_LOGLEVEL) | ||
| def make_server_store() -> Path: | ||
| path = tempfile.mkdtemp(prefix="tmserver.", dir="/tmp") | ||
| logger.debug("Created server store: %s", path) | ||
| return path | ||
|
|
||
| server_store: str = make_server_store() | ||
| cmd: List[str] = ["task-maker-tools"] | ||
| if verbosity_flag: | ||
| cmd.append(verbosity_flag) | ||
| cmd += ["server", "--store-dir", server_store] | ||
| if SERVER_ARGS.strip(): | ||
| cmd += shlex.split(SERVER_ARGS) | ||
|
|
||
| logger.debug("Exec: %s", " ".join(shlex.quote(c) for c in cmd)) | ||
| logger.info("Starting server (store=%s)…", server_store) | ||
| rc = subprocess.call(cmd) | ||
| logger.info("Server exited with rc=%d", rc) | ||
| return rc | ||
|
|
||
| def spawn_tmworker(store_dir: str) -> subprocess.Popen[Any]: | ||
| verbosity_flag: str = loglevel_verbosity_flag(TM_LOGLEVEL) | ||
|
|
||
| cmd: List[str] = ["task-maker-tools"] | ||
| if verbosity_flag: | ||
| cmd.append(verbosity_flag) | ||
| cmd += ["worker", "--store-dir", store_dir] | ||
| if WORKER_ARGS.strip(): | ||
| cmd += shlex.split(WORKER_ARGS) | ||
| cmd.append(SERVER_ADDR) | ||
|
|
||
| logger.debug("Exec: %s", " ".join(shlex.quote(c) for c in cmd)) | ||
| logger.info("Starting worker (store=%s) → %s", store_dir, SERVER_ADDR) | ||
| proc = subprocess.Popen(cmd) | ||
| logger.debug("Worker PID %s started for store %s", proc.pid, store_dir) | ||
| return proc | ||
|
|
||
| # ---------- main ---------- | ||
| def main() -> int: | ||
| configure_logging() | ||
| args = cli() | ||
| nworkers: int = int(args.jobs) | ||
|
|
||
| logger.info( | ||
| f"Config: jobs={nworkers}, server={SPAWN_SERVER}, worker={SPAWN_WORKERS}, " | ||
| f"addr={SERVER_ADDR}, tm_loglevel={TM_LOGLEVEL}" | ||
| ) | ||
| if SERVER_ARGS.strip(): | ||
| logger.debug("SERVER_ARGS=%r", SERVER_ARGS) | ||
| if WORKER_ARGS.strip(): | ||
| logger.debug("WORKER_ARGS=%r", WORKER_ARGS) | ||
|
|
||
| # create nworkers file in the current dir and save a 0 into it | ||
| nworkers_file: Path = Path('nworkers') | ||
| with nworkers_file.open('w') as nw_fp: | ||
| nw_fp.write("0\n") | ||
|
|
||
| if SPAWN_WORKERS: | ||
| worker_stores: List[str] = make_worker_stores(nworkers) | ||
| # overwrite ith the actual number of wokers if we spawn them | ||
| with nworkers_file.open('w') as nw_fp: | ||
| nw_fp.write(f"{nworkers}\n") | ||
|
|
||
| # worker only | ||
| if (not SPAWN_SERVER) and SPAWN_WORKERS: | ||
| logger.info("Mode: workers only.") | ||
| procs: List[subprocess.Popen[Any]] = [spawn_tmworker(store_dir=w) | ||
| for w in worker_stores] | ||
| exit_codes: List[Optional[int]] = [p.wait() for p in procs] | ||
| max_rc = max(code or 0 for code in exit_codes) | ||
| logger.info("All workers exited. Max rc=%d", max_rc) | ||
| return max_rc | ||
|
|
||
| # server only | ||
| elif SPAWN_SERVER and (not SPAWN_WORKERS): | ||
| logger.info("Mode: server only.") | ||
| return spawn_tmserver() | ||
|
|
||
| # server + worker | ||
| elif SPAWN_SERVER and SPAWN_WORKERS: | ||
| logger.info("Mode: server + workers.") | ||
| procs: List[subprocess.Popen[Any]] = [] | ||
|
|
||
| def launch_workers() -> None: | ||
| logger.debug("Delaying worker launch by 2s to let server come up…") | ||
| time.sleep(2.0) | ||
| for w in worker_stores: | ||
| procs.append(spawn_tmworker(store_dir=w)) | ||
| for p in procs: | ||
| rc = p.wait() | ||
| logger.info("Worker PID %s exited with rc=%d", p.pid, rc) | ||
|
|
||
| t = threading.Thread(target=launch_workers, daemon=True) | ||
| t.start() | ||
| server_rc: int = spawn_tmserver() | ||
|
|
||
| logger.debug("Server finished (rc=%d). Joining worker launcher…", server_rc) | ||
| t.join(timeout=1.0) | ||
| # If any workers still running, terminate politely | ||
| for p in procs: | ||
| if p.poll() is None: | ||
| logger.debug("Terminating lingering worker PID %s…", p.pid) | ||
| try: | ||
| p.terminate() | ||
| except Exception as e: | ||
| logger.warning("Failed to terminate worker PID %s: %s", p.pid, e) | ||
| return server_rc | ||
|
|
||
| # nothing to spawn -> shell | ||
| else: | ||
| logger.info("Mode: nothing to spawn — opening interactive shell.") | ||
| shell: str = os.environ.get("SHELL", "/bin/bash") | ||
| try: | ||
| return subprocess.call([shell]) | ||
| except FileNotFoundError: | ||
| logger.error("Shell %r not found; exiting 0.", shell) | ||
| return 0 | ||
|
|
||
| if __name__ == "__main__": | ||
| try: | ||
| sys.exit(main()) | ||
| except subprocess.CalledProcessError as e: | ||
| logger.exception("Subprocess failed (rc=%s).", e.returncode) | ||
| sys.exit(e.returncode) | ||
| except Exception: | ||
| logger.exception("Fatal error:") | ||
| sys.exit(1) |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This Docker image is:
I think it would be better to either:
.debfile that is already built by the CI.¹ If rustup downloads a newer version of Rust, the build may fail.
Given the use case of this Docker image (a quick and easy way of deploying task-maker), I'm biased to solution 1.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not both? I have created another
Dockerfilewhere ICOPYmy local repository and buildtask-maker-rust. I think it would be nice to provide a image for uses that want to use task-maker-rust as soon as possible and another for the people interested in developing and contributing.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@edomora97 wrote:
Regarding the speed of build, this is a bit faster than the previous version (building from source), but it's still not super fast as on my machine it takes ~ 10 minutes (against 16.5 minutes).