This commit is contained in:
Thomas Hobson 2021-04-25 20:55:20 +12:00
parent 436c7f3c25
commit 3928bace86
No known key found for this signature in database
GPG Key ID: 9F1FD9D87950DB6F
7 changed files with 224 additions and 230 deletions

View File

@ -6,7 +6,6 @@
"dependencies": { "dependencies": {
"body-parser": "^1.19.0", "body-parser": "^1.19.0",
"express": "^4.17.1", "express": "^4.17.1",
"express-validator": "^6.10.0",
"is-docker": "^2.1.1", "is-docker": "^2.1.1",
"js-yaml": "^4.0.0", "js-yaml": "^4.0.0",
"logplease": "^1.2.15", "logplease": "^1.2.15",

184
api/src/api/v1.js Normal file
View File

@ -0,0 +1,184 @@
const express = require('express');
const router = express.Router();
const runtime = require('../runtime');
const {Job} = require("../job");
const package = require('../package')
const logger = require('logplease').create('api/v1');
router.post('/execute', async function(req, res){
const {language, version, files, stdin, args, run_timeout, compile_timeout} = req.body;
if(!language || typeof language !== "string")
{
return res
.status(400)
.send({
message: "language is required as a string"
});
}
if(!version || typeof version !== "string")
{
return res
.status(400)
.send({
message: "version is required as a string"
});
}
if(!files || !Array.isArray(files))
{
return res
.status(400)
.send({
message: "files is required as an array"
});
}
for (const [i,file] of files.entries()) {
if(!file.content || typeof file.content !== "string"){
return res
.status(400)
.send({
message: `files[${i}].content is required as a string`
});
}
}
const rt = runtime.get_latest_runtime_matching_language_version(language, version);
if (rt === undefined) {
return res
.status(400)
.send({
message: `${language}-${version} runtime is unknown`
});
}
const job = new Job({
runtime: rt,
alias: language,
files: files,
args: args || [],
stdin: stdin || "",
timeouts: {
run: run_timeout || 3000,
compile: compile_timeout || 10000
}
});
await job.prime();
const result = await job.execute();
await job.cleanup();
return res
.status(200)
.send(result);
});
router.get('/runtimes', function(req, res){
const runtimes = runtime.map(rt => ({
language: rt.language,
version: rt.version.raw,
aliases: rt.aliases,
runtime: rt.runtime
}));
return res
.status(200)
.send(runtimes);
});
router.get('/packages', async function(req, res){
logger.debug('Request to list packages');
let packages = await package.get_package_list();
packages = packages
.map(pkg => {
return {
language: pkg.language,
language_version: pkg.version.raw,
installed: pkg.installed
};
});
return res
.status(200)
.send(packages);
});
router.post('/packages/:language/:version', async function(req, res){
logger.debug('Request to install package');
const {language, version} = req.params;
const pkg = await package.get_package(language, version);
if (pkg == null) {
return res
.status(404)
.send({
message: `Requested package ${language}-${version} does not exist`
});
}
try {
const response = await pkg.install();
return res
.status(200)
.send(response);
} catch(e) {
logger.error(`Error while installing package ${pkg.language}-${pkg.version}:`, e.message);
return res
.status(500)
.send({
message: e.message
});
}
});
router.delete('/packages/:language/:version', async function(req, res){
logger.debug('Request to uninstall package');
const {language, version} = req.params;
const pkg = await package.get_package(language, version);
if (pkg == null) {
return res
.status(404)
.send({
message: `Requested package ${language}-${version} does not exist`
});
}
try {
const response = await pkg.uninstall();
return res
.status(200)
.send(response);
} catch(e) {
logger.error(`Error while uninstalling package ${pkg.language}-${pkg.version}:`, e.message);
return res
.status(500)
.send({
message: e.message
});
}
});
module.exports = router;

View File

@ -1,57 +0,0 @@
// {"language":"python","version":"3.9.1","files":{"code.py":"print('hello world')"},"args":[],"stdin":"","compile_timeout":10, "run_timeout":3}
// {"success":true, "run":{"stdout":"hello world", "stderr":"", "error_code":0},"compile":{"stdout":"","stderr":"","error_code":0}}
const { get_latest_runtime_matching_language_version } = require('../runtime');
const { Job } = require('./job');
const { body } = require('express-validator');
module.exports = {
run_job_validators: [
body('language')
.isString(),
body('version')
.isString(),
// isSemVer requires it to be a version, not a selector
body('files')
.isArray(),
body('files.*.content')
.isString(),
],
// POST /execute
async run_job(req, res) {
const runtime = get_latest_runtime_matching_language_version(req.body.language, req.body.version);
if (runtime === undefined) {
return res
.status(400)
.send({
message: `${req.body.language}-${req.body.version} runtime is unknown`
});
}
const job = new Job({
runtime,
alias: req.body.language,
files: req.body.files,
args: req.body.args || [],
stdin: req.body.stdin || "",
timeouts: {
run: req.body.run_timeout || 3000,
compile: req.body.compile_timeout || 10000
}
});
await job.prime();
const result = await job.execute();
await job.cleanup();
return res
.status(200)
.send(result);
}
};

View File

@ -9,7 +9,6 @@ const fs = require('fs/promises');
const fss = require('fs'); const fss = require('fs');
const body_parser = require('body-parser'); const body_parser = require('body-parser');
const runtime = require('./runtime'); const runtime = require('./runtime');
const { validationResult } = require('express-validator');
const logger = Logger.create('index'); const logger = Logger.create('index');
const app = express(); const app = express();
@ -67,48 +66,10 @@ const app = express();
}) })
}) })
const validate = (req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res
.status(400)
.send({
message: errors.array()
});
}
next();
};
logger.debug('Registering Routes'); logger.debug('Registering Routes');
const ppman_routes = require('./ppman/routes'); const api_v1 = require('./api/v1')
const executor_routes = require('./executor/routes'); app.use('/api/v1', api_v1);
app.get('/api/v1/packages', ppman_routes.package_list);
app.post('/api/v1/packages/:language/:version', ppman_routes.package_install);
app.delete('/api/v1/packages/:language/:version', ppman_routes.package_uninstall);
app.post('/api/v1/execute',
executor_routes.run_job_validators,
validate,
executor_routes.run_job
);
app.get('/api/v1/runtimes', (req, res) => {
const runtimes = runtime
.map(rt => {
return {
language: rt.language,
version: rt.version.raw,
aliases: rt.aliases,
runtime: rt.runtime
};
});
return res
.status(200)
.send(runtimes);
});
app.use(function (req,res,next){ app.use(function (req,res,next){
return res.status(404).send({message: 'Not Found'}); return res.status(404).send({message: 'Not Found'});

View File

@ -2,8 +2,8 @@ const logger = require('logplease').create('executor/job');
const {v4: uuidv4} = require('uuid'); const {v4: uuidv4} = require('uuid');
const cp = require('child_process'); const cp = require('child_process');
const path = require('path'); const path = require('path');
const config = require('../config'); const config = require('./config');
const globals = require('../globals'); const globals = require('./globals');
const fs = require('fs/promises'); const fs = require('fs/promises');
const job_states = { const job_states = {

View File

@ -1,14 +1,14 @@
const logger = require('logplease').create('ppman/package'); const logger = require('logplease').create('ppman/package');
const semver = require('semver'); const semver = require('semver');
const config = require('../config'); const config = require('./config');
const globals = require('../globals'); const globals = require('./globals');
const fetch = require('node-fetch'); const fetch = require('node-fetch');
const path = require('path'); const path = require('path');
const fs = require('fs/promises'); const fs = require('fs/promises');
const fss = require('fs'); const fss = require('fs');
const cp = require('child_process'); const cp = require('child_process');
const crypto = require('crypto'); const crypto = require('crypto');
const runtime = require('../runtime'); const runtime = require('./runtime');
class Package { class Package {
@ -158,8 +158,38 @@ class Package {
} }
static async get_package_list() {
const repo_content = await fetch(config.repo_url).then(x => x.text());
const entries = repo_content
.split('\n')
.filter(x => x.length > 0);
return entries.map(line => {
const [ language, version, checksum, download ] = line.split(',', 4);
return new Package({
language,
version,
checksum,
download
});
});
}
static async get_package (lang, version) {
const packages = await Package.get_package_list();
const candidates = packages
.filter(pkg => {
return pkg.language == lang && semver.satisfies(pkg.version, version)
});
candidates.sort((a, b) => semver.rcompare(a.version, b.version));
return candidates[0] || null;
}
} }
module.exports = { module.exports = Package;
Package
};

View File

@ -1,123 +0,0 @@
const logger = require('logplease').create('ppman/routes');
const semver = require('semver');
const fetch = require('node-fetch');
const config = require('../config');
const { Package } = require('./package');
const get_package_list = async () => {
const repo_content = await fetch(config.repo_url).then(x => x.text());
const entries = repo_content
.split('\n')
.filter(x => x.length > 0);
return entries.map(line => {
const [ language, version, checksum, download ] = line.split(',', 4);
return new Package({
language,
version,
checksum,
download
});
});
};
const get_package = async (lang, version) => {
const packages = await get_package_list();
const candidates = packages
.filter(pkg => {
return pkg.language == lang && semver.satisfies(pkg.version, version)
});
candidates.sort((a, b) => semver.rcompare(a.version, b.version));
return candidates[0] || null;
};
module.exports = {
// GET /packages
async package_list(req, res) {
logger.debug('Request to list packages');
let packages = await get_package_list();
packages = packages
.map(pkg => {
return {
language: pkg.language,
language_version: pkg.version.raw,
installed: pkg.installed
};
});
return res
.status(200)
.send(packages);
},
// POST /packages/:language/:version
async package_install(req, res) {
logger.debug('Request to install package');
const pkg = await get_package(req.params.language, req.params.version);
if (pkg == null) {
return res
.status(404)
.send({
message: `Requested package ${req.params.language}-${req.params.version} does not exist`
});
}
try {
const response = await pkg.install();
return res
.status(200)
.send(response);
} catch(e) {
logger.error(`Error while installing package ${pkg.language}-${pkg.version}:`, e.message);
return res
.status(500)
.send({
message: e.message
});
}
},
// DELETE /packages/:language/:version
async package_uninstall(req, res) {
logger.debug('Request to uninstall package');
const pkg = await get_package(req.params.language, req.params.version);
if (pkg == null) {
return res
.status(404)
.send({
message: `Requested package ${req.params.language}-${req.params.version} does not exist`
});
}
try {
const response = await pkg.uninstall();
return res
.status(200)
.send(response);
} catch(e) {
logger.error(`Error while uninstalling package ${pkg.language}-${pkg.version}:`, e.message);
return res
.status(500)
.send({
message: e.message
});
}
}
};