-
Notifications
You must be signed in to change notification settings - Fork 2
Add job runner system with API and database models #125
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
base: main
Are you sure you want to change the base?
Conversation
Introduces a job runner service that polls for pending jobs, executes commands, and records output/status. Adds Sequelize models and migrations for Jobs and JobStatuses, a jobs API router for job management, and integrates the router into the server. Also includes a systemd service file and updates package.json scripts.
Introduces an asynchronous job runner for the create-a-container service, including Sequelize models, migrations, a background job-runner process, new API endpoints for job management, and a systemd unit file. This enables long-running tasks to be executed outside HTTP lifecycles, with progress reporting and admin-only job creation for security.
… the user who created each job.
| router.use(requireAuth); | ||
|
|
||
| // POST /jobs - enqueue a new job (admins only) | ||
| router.post('/', async (req, res) => { |
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.
(1) I thought we were going to remove this route and let other routes add jobs as needed
(2) This needs the requireAdmin middleware if we're going to keep it to prevent anyone from queuing arbitrary jobs.
| const username = req.session && req.session.user; | ||
| const isAdmin = req.session && req.session.isAdmin; | ||
| if (!isAdmin && job.createdBy !== username) { | ||
| return res.status(403).json({ error: 'Forbidden' }); |
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.
We should 404 if a user is trying to access a job they didn't make. 403 leaks information.
| const offset = req.query.offset ? Math.max(0, parseInt(req.query.offset, 10)) : null; | ||
| const limit = req.query.limit ? Math.min(1000, parseInt(req.query.limit, 10)) : 1000; | ||
|
|
||
| const Op = require('sequelize').Op; |
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.
Pull sequelize in from the models folder on line 3. I.e. const { Job, JobStatus, sequelize } = require('../models'); rather than requiring in the middle of the file. (As a rule, all requires should be at the top of the file).
| const where = { jobId: id }; | ||
| const findOpts = { where, order: [['createdAt', 'ASC']], limit }; | ||
|
|
||
| if (sinceId) { |
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.
Thank you for trying to implement limit/offset, but we should pick a canonical way to do it rather than trying to support both limit/offset and sinceId.
| const username = req.session && req.session.user; | ||
| const isAdmin = req.session && req.session.isAdmin; | ||
| if (!isAdmin && job.createdBy !== username) { | ||
| return res.status(403).json({ error: 'Forbidden' }); |
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.
Ditto above. 404 for jobs not owned by the user to avoid leaking information.
Issue: #119
Jobs / job-runner Feature
This document summarizes the new Jobs feature, how it works, how to deploy it, and how to test it. It's intended to be pasted into the PR description or included in the
create-a-containerdocs.Overview
This change introduces an asynchronous job system for the
create-a-containerservice:Jobstable: stores queued commands and status (pending,running,success,failure,cancelled).JobStatusestable: stores timestamped output logs for each job.job-runner.js: a small service that runs with the same DB/config as the API server. It claimspendingjobs, executes the configuredcommandin a subprocess, streams stdout/stderr intoJobStatuses, and updates job status on exit./api/jobs:POST /api/jobs— enqueue a job (admins only).GET /api/jobs/:id— job metadata (id, command, status, timestamps).GET /api/jobs/:id/status— returns log rows; supportssinceIdandlimitquery params for incremental polling.job-runner.service— systemd unit file (added to repo) to run the runner as a system service.Files changed / added
models/job.js— Sequelize Job modelmodels/jobstatus.js— Sequelize JobStatus modelmigrations/20251117120000-create-jobs.js— migration for Jobsmigrations/20251117120001-create-jobstatuses.js— migration for JobStatusesjob-runner.js— the runner servicejob-runner.service— example systemd unitrouters/jobs.js— new API endpoints; POST restricted to adminsserver.js— mounts/api/jobsSecurity & Access Control
POST /api/jobsis restricted to admin users via the existingrequireAdminmiddleware. Other job endpoints require authentication (requireAuth) but are readable by authenticated non-admin users.commandstring. Do NOT expose this to untrusted users. Enqueue jobs only from trusted server-side code or admin UI.For long-term security, we can change
POST /api/jobsto accepttask+paramsinstead of raw commands, and map tasks to safe server-side scripts.Database migration
Run migrations from the
create-a-containerdirectory:cd create-a-container npm run db:migrateThis will create
JobsandJobStatusestables. Ensure your DB user has privileges to ALTER CREATE tables.Running the job-runner
The runner works with the same environment as
server.js. Example manual startup (fromcreate-a-container):To run as a systemd service on the host (recommended for production):
/opt/container-creatoror adjust paths).If you provide environment variables via
/etc/default/container-creator, addEnvironmentFile=/etc/default/container-creatorto the unit file.Important env variables
job-runner.jsrespects these environment variables:JOB_RUNNER_POLL_MS— poll interval in ms (default 2000)JOB_RUNNER_CWD— working directory for spawned jobs (defaults to service cwd)The runner also uses your DB config from
config/config.js(which in-turn uses.env). Ensure DB env vars (MYSQL_HOST,MYSQL_USER,MYSQL_PASSWORD,MYSQL_DATABASE) are set.How to enqueue a job (admin)
From the UI (recommended): sign in as an admin and use the admin UI that enqueues jobs server-side.
Using
curlwith session cookie (example):cookies.txt.Response:
{ "id": 123, "status": "pending" }Fetching job logs
Poll for job statuses (incremental polling):
The API returns an array of objects:
{ id, output, createdAt }. UsesinceIdto avoid re-downloading old logs.Frontend streaming / status page
The existing frontend
views/status.htmlcurrently talks to an in-memoryjobsobject. With the new persistent job system you should:GET /api/jobs/:id/statuswithsinceIdand append new output lines.JobStatusrows as they are created. The current implementation supports polling and incremental reads.Testing plan
curllogin + POST as described above.Jobsrow created withpendingstatus.job-runnerpicks up the job (watch journalctl or runner stdout) and job status becomesrunning.JobStatusesrows appear and contain stdout/stderr chunks.successorfailureon exit.GET /api/jobs/:id/statusreturns the accumulated log rows.Rollback
To remove the feature, revert this pull request and run migrations to drop
JobStatusesandJobstables (or run the down migration):(Adjust migration undo commands according to your migration tooling.)
Future work / improvements
commandstrings with task identifiers + params to avoid arbitrary shell execution.MAX_WORKERS).