diff --git a/api/package.json b/api/package.json index 819a5c5..75cb08c 100644 --- a/api/package.json +++ b/api/package.json @@ -6,6 +6,7 @@ "dependencies": { "body-parser": "^1.19.0", "express": "^4.17.1", + "express-validator": "^6.10.0", "is-docker": "^2.1.1", "js-yaml": "^4.0.0", "logplease": "^1.2.15", diff --git a/api/src/api/v1.js b/api/src/api/v1.js deleted file mode 100644 index 72db74e..0000000 --- a/api/src/api/v1.js +++ /dev/null @@ -1,184 +0,0 @@ -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; diff --git a/api/src/job.js b/api/src/executor/job.js similarity index 97% rename from api/src/job.js rename to api/src/executor/job.js index d68ad98..63122d0 100644 --- a/api/src/job.js +++ b/api/src/executor/job.js @@ -1,9 +1,9 @@ -const logger = require('logplease').create('job'); +const logger = require('logplease').create('executor/job'); const {v4: uuidv4} = require('uuid'); const cp = require('child_process'); const path = require('path'); -const config = require('./config'); -const globals = require('./globals'); +const config = require('../config'); +const globals = require('../globals'); const fs = require('fs/promises'); const job_states = { diff --git a/api/src/executor/routes.js b/api/src/executor/routes.js new file mode 100644 index 0000000..d7cabff --- /dev/null +++ b/api/src/executor/routes.js @@ -0,0 +1,57 @@ +// {"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); + } + +}; diff --git a/api/src/index.js b/api/src/index.js index 8a3fbb4..fcf82d6 100644 --- a/api/src/index.js +++ b/api/src/index.js @@ -9,6 +9,7 @@ const fs = require('fs/promises'); const fss = require('fs'); const body_parser = require('body-parser'); const runtime = require('./runtime'); +const { validationResult } = require('express-validator'); const logger = Logger.create('index'); const app = express(); @@ -66,10 +67,48 @@ 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'); - const api_v1 = require('./api/v1') - app.use('/api/v1', api_v1); + const ppman_routes = require('./ppman/routes'); + const executor_routes = require('./executor/routes'); + + 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){ return res.status(404).send({message: 'Not Found'}); diff --git a/api/src/package.js b/api/src/ppman/package.js similarity index 81% rename from api/src/package.js rename to api/src/ppman/package.js index ec68f46..0e3c994 100644 --- a/api/src/package.js +++ b/api/src/ppman/package.js @@ -1,14 +1,14 @@ -const logger = require('logplease').create('package'); +const logger = require('logplease').create('ppman/package'); const semver = require('semver'); -const config = require('./config'); -const globals = require('./globals'); +const config = require('../config'); +const globals = require('../globals'); const fetch = require('node-fetch'); const path = require('path'); const fs = require('fs/promises'); const fss = require('fs'); const cp = require('child_process'); const crypto = require('crypto'); -const runtime = require('./runtime'); +const runtime = require('../runtime'); class Package { @@ -158,38 +158,8 @@ 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 = Package; +module.exports = { + Package +}; diff --git a/api/src/ppman/routes.js b/api/src/ppman/routes.js new file mode 100644 index 0000000..89f472c --- /dev/null +++ b/api/src/ppman/routes.js @@ -0,0 +1,123 @@ +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 + }); + } + } + +}; diff --git a/packages/deno/1.7.5/metadata.json b/packages/deno/1.7.5/metadata.json index d30608b..60b8a65 100644 --- a/packages/deno/1.7.5/metadata.json +++ b/packages/deno/1.7.5/metadata.json @@ -1,14 +1,5 @@ { "language": "deno", "version": "1.7.5", - "provides": [ - { - "language": "typescript", - "aliases": ["deno-ts","deno"] - }, - { - "language": "javascript", - "aliases": ["deno-js"] - } - ] + "aliases": ["deno-ts", "deno-js"] } diff --git a/packages/test.sh b/packages/test.sh index 615e2db..9ba39ba 100755 --- a/packages/test.sh +++ b/packages/test.sh @@ -17,5 +17,5 @@ do echo "==$test_file: $language-$lang_ver==" #jq '.' <<<"$result" - jq -r 'if (.run.stdout | contains("OK") ) then (.run.stdout) else (.compile.output + .run.output) end' <<<$result + jq -r '.compile.output + .run.output' <<<$result done diff --git a/test.py b/test.py new file mode 100644 index 0000000..b376c99 --- /dev/null +++ b/test.py @@ -0,0 +1 @@ +print('hello')