diff --git a/app/ProxyRouter.js b/app/ProxyRouter.js index 87b9e6e08e6a67548bcbaa7dbcd0ababe1a2ed6b..c72a7d5d6ba2ba079e1ce1b50442b2761610734e 100644 --- a/app/ProxyRouter.js +++ b/app/ProxyRouter.js @@ -1,3 +1,4 @@ +const config = require('config') const stringify = require('json-stringify-safe'); const http = require('http'); const fs = require('fs'); @@ -6,8 +7,6 @@ const yaml = require('js-yaml') const k8 = require('./kubernetes'); const logger = require('./logger') -const reloadMsg = `<html><head><title>Starting up!</title><meta http-equiv="refresh" content="${config.app.pageReloadTime}" ></head><body><h3>Please wait while we start a container for you!</h3><p>You might need to refresh manually (F5)...</body></html>`; - function guaranteeDir(path, next) { fs.access(path, fs.constants.F_OK | fs.constants.R_OK, (err) => { if(err){ @@ -41,6 +40,7 @@ function getOrCreatePod(podName, repl, shouldCreate, next) { k8.ns(config.k8component.namespace).pod.get(podName, function(err, result) { if(err) { if (shouldCreate) { + logger.debug(`creating ${podName}`) components.templateForImage(repl, function(err, template, repl) { if(err) { logger.error(`Cannot start pod ${podName}, error in template generation: ${stringify(err)}`); @@ -50,7 +50,7 @@ function getOrCreatePod(podName, repl, shouldCreate, next) { const templateValue = yaml.safeLoad(template, 'utf8') k8.ns(config.k8component.namespace).pod.post({ body: templateValue}, function(err, res2){ if(err) { - logger.error(`Cannot start pod ${podName}, error: ${stringify(err)}, \n====\ntemplate was ${template}\n====\nexpanded to\n${templateValue}\n====`); + logger.error(`Cannot start pod ${podName}, error: ${stringify(err)}, \n====\ntemplate was ${template}\n====`); next(err, null) } else { logger.info(`Created pod ${podName}: ${stringify(res2)}`) @@ -108,22 +108,28 @@ function resolvePod(repl, next) { resolveCache.set(podName, res) next(null, res) } else { + let secondsSinceCreation = (Date.now() - Date.parse(pod.metadata.creationTimestamp))/ 1000.0 const err = { error: "not ready", msg: "pod not yet ready", status: pod.status, host: podIp, - port: portNr + port: portNr, + pod: pod, + secondsSinceCreation: secondsSinceCreation } next(err, null) } } else { + let secondsSinceCreation = (Date.now() - Date.parse(pod.metadata.creationTimestamp))/ 1000.0 const err = { error: "no ip", msg: "ip not yet available", status: pod.status, host: podIp, - port: portNr + port: portNr, + pod: pod, + secondsSinceCreation: secondsSinceCreation } next(err, null) } @@ -147,22 +153,39 @@ ProxyRouter.prototype.lookup = function(req, res, userID, isWebsocket, path, nex components.cachedReplacements(req, function(err, repl) { //logger.debug(`replacements available after ${(Date.now()-start)/1000.0}s`) if (err) { - logger.error(`No replacements: lookup without visiting the entry point ${config.k8component.entryPoint.path} (${stringify(err)})`) + logger.error(`no replacements for ${userID} in %{path}`) + res.send(500, components.getHtmlErrorTemplate({ + error:"No replacements", + msg: `lookup without visiting the entry point ${config.k8component.entryPoint.path} (${stringify(err)})` + })) } else { resolvePod(repl, function (err, target) { - //logger.debug(`target available after ${(Date.now()-start)/1000.0}s`) + //logger.debug(`target available after ${(Date.now()-start)/1000.0}s, err: ${stringify(err)} target: ${stringify(target)}`) if (err) { - if (err.error === 'no ip' && err.status && err.status.phase === 'Pending' || - err.error === 'not ready') { - logger.warn(`pod ${repl.podName} ${err.error} ${stringify(err)}`) - res.send(reloadMsg) + if ((err.error === 'no ip' || err.error === 'not ready') && + err.status && (err.status.phase === 'Pending' || err.status.phase === 'Running')) { + let error_detail = '' + if (!err.secondsSinceCreation || err.secondsSinceCreation > 15) + error_detail = stringify(err, null, 2) + let repl = { + refreshEachS: config.app.pageReloadTime, + error_detail: error_detail + } + components.evalHtmlTemplate( + 'reloadMsg.html', repl, + function(err, pageHtml) { + if (res && res.send) + res.send(pageHtml) + }) + return; } else { - const errorMsg = `<html><head><title>Error starting Container!</title><meta http-equiv="refresh"<body><h3>Error ${err.error} while trying to start a container for you!</h3><p>${err.msg}</p><pre>${stringify(err, null, 2 )}</pre></body></html>`; logger.error(`error starting container ${repl.podName}: ${stringify(err)}`) - res.send(500, errorMsg) + let errorHtml = components.getHtmlErrorTemplate(err, "Error starting container") + logger.error(`errorHtml: ${stringify(errorHtml)}`) + if (res && res.status && res.send) + res.status(500).send(errorHtml) } } else { - // logger.debug(`Resolved to ${stringify(target)} after ${(Date.now()-start)/1000.0}s`) next(target); } }) diff --git a/app/components.js b/app/components.js index 0d4ed90580beae82acb10b1ad7949989e79311ee..edf5e9ae265fbae192df4e91c41520da7a8067bf 100644 --- a/app/components.js +++ b/app/components.js @@ -1,4 +1,4 @@ -config = require('config') +const config = require('config') const handlebars = require('handlebars'); const fs = require('fs'); const path = require('path'); @@ -10,6 +10,7 @@ const url = require('url'); const compact_sha = require('./compact-sha') const logger = require('./logger') const stringify = require('json-stringify-safe') +const yaml = require('js-yaml') var baseRepl = { baseDir: baseDir, @@ -18,7 +19,15 @@ var baseRepl = { }; const br = config.app.baseReplacements for (k in br) - baseRepl[k] = br[k]; + baseRepl[k] = br[k]; + +handlebars.registerHelper('json', function(object){ + return new Handlebars.SafeString(stringify(object)); +}); + +handlebars.registerHelper('prettyJson', function(object){ + return stringify(object, null, 2); +}); // Create a template from the given string function templatize(str) { @@ -110,7 +119,7 @@ function evalTemplate(templatePath, extraRepl, next) { // Immediately returns an html describing the error function getHtmlErrorTemplate(err, context = '') { let error = '', error_msg = '', error_detail = '' - if (!err) { + if (err) { try { error_detail = stringify(err, null, 2) error = err.error || '' @@ -134,25 +143,40 @@ function getHtmlErrorTemplate(err, context = '') { </pre> </body> </html>` + } else { + return '' } } // Helper to evaluate a web page template (layout + content) // will *always* give an html as result (it there was an error it describe the error -function evalHtmlTemplate(htmlPath, repl, next, layout = null, context = '') { - const layout = repl.layout || "defaultTemplate.html" - evalTemplate("html/"+htmlPath, repl, function (err, template){ +function evalHtmlTemplate(htmlPath, repl, next, { context = '' } = {} ) { + //logger.debug(`entering evalHtmlTemplate(${stringify(htmlPath)}, ${stringify(repl)},...)`) + evalTemplate('html/'+htmlPath, repl, function (err, template){ if (err) { + logger.warn(`evalTemplate ${htmlPath} returning error ${stringify(err)}`) next(err, getHtmlErrorTemplate(err, context)) } else { - const repl2 = Object.assign({title: htmlPath, head: ''}, repl, { body: template }) - evalHtmlTemplate("html/"+layout, repl2, function(err,res){ - if (err) { - next(err, getHtmlErrorTemplate(err, context)) - } else { - next(nil, res) - } - }) + let extraRepl = {} + let templateBody = template + let m = /\B---\B/.exec(template) + if (m) { + templateBody = template.slice(m.index + 3) + extraRepl = yaml.safeLoad(template.slice(0, m.index), 'utf8') + } + const repl2 = Object.assign({title: htmlPath, head: '', layout: "defaultLayout.html"}, extraRepl, repl, { body: templateBody }) + const layout = repl2.layout + if (layout) { + evalTemplate("htmlLayout/"+layout, repl2, function(err,res){ + if (err) { + next(err, getHtmlErrorTemplate(err, context)) + } else { + next(null, res) + } + }) + } else { + next(null, templateBody) + } } }) } @@ -276,5 +300,7 @@ module.exports = { cachedReplacements: cachedReplacements, podNameForRepl: podNameForRepl, infoForPodName: infoForPodName, - templateForImage: templateForImage + templateForImage: templateForImage, + getHtmlErrorTemplate: getHtmlErrorTemplate, + evalHtmlTemplate: evalHtmlTemplate } diff --git a/templates/html/defaultLayout.html b/templates/html/defaultLayout.html deleted file mode 100644 index 1268542bd1123fe278e098d6d2cca6a8b677c68c..0000000000000000000000000000000000000000 --- a/templates/html/defaultLayout.html +++ /dev/null @@ -1,11 +0,0 @@ -<!doctype html> -<html> - <head> - <title>{{title}}</title> - <meta charset="utf-8" /> - {{head}} - </head> - <body> - {{body}} - </body> -</html> diff --git a/templates/html/reloadMsg.html b/templates/html/reloadMsg.html index 1be3159853ba4746c25c9b60813801b1f8add14c..2f1ce7bb5a192570f7afe264e85d938cb0a741b0 100644 --- a/templates/html/reloadMsg.html +++ b/templates/html/reloadMsg.html @@ -1,2 +1,8 @@ +title: "Starting up!" +head: "<meta http-equiv=\"refresh\" content=\"{{refreshEachS}}\" >" +--- <h3>Please wait while we start a container for you!</h3> -<p>You might need to refresh manually (F5)...</p>p> +<p>You might need to refresh manually (F5)...</p> +<pre> +{{error_detail}} +</pre> diff --git a/templates/htmlLayout/defaultLayout.html b/templates/htmlLayout/defaultLayout.html new file mode 100644 index 0000000000000000000000000000000000000000..61661092082cc12577ec39d2d6ee8254b0e332a2 --- /dev/null +++ b/templates/htmlLayout/defaultLayout.html @@ -0,0 +1,15 @@ +<!doctype html> +<html> + <head> + <title>{{{title}}}</title> + <meta charset="utf-8" /> + <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous"> + {{{head}}} + </head> + <body> + {{{body}}} + <script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script> + <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js" integrity="sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49" crossorigin="anonymous"></script> + <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.min.js" integrity="sha384-ChfqqxuZUCnJSK3+MXmPNIyE6ZbWh2IMqE241rYiqJxyMiZ6OW/JmZQ5stwEULTy" crossorigin="anonymous"></script> + </body> +</html>