Commit 39274171 authored by Mohamed, Fawzi Roberto (fawzi)'s avatar Mohamed, Fawzi Roberto (fawzi)
Browse files

entry point, image type through config NODE_APP_INSTANCE, safe-memory-cache submodule

parent 0c336f67
[submodule "safe-memory-cache"]
path = safe-memory-cache
url = https://github.com/fawzi/safe-memory-cache.git
var env = process.env.NODE_ENV || 'development';
const config = require('config') // require('./config/config')[env];
console.log('Using configuration', config.util.getEnv('NODE_ENV'), config);
function main() {
var env = process.env.NODE_ENV || 'development';
var iarg = 2
var args = process.argv
var cmds = []
var imageType = config.k8component.imageType
const usage = `node ${args[1]} [-h|--help] [--image-type [beaker|jupyter|creedo|remotevis]] [webserver|watcher]
default imageType for current config = ${imageType}`
console.log(`Started with arguments ${JSON.stringify(args)}`)
`
console.log(`Started with arguments ${JSON.stringify(args)}`)
var imageType = undefined
while (iarg < args.length) {
var arg = args[iarg]
iarg += 1
......@@ -31,10 +28,18 @@ function main() {
} else if (arg == "watcher") {
cmds.push("watcher")
} else {
throw new Error(`unknown command line argument '${arg}.\n${usage}'`)
throw new Error(`unknown command line argument '${arg}'.\n${usage}`)
}
}
config.k8component['imageType'] = imageType
if (imageType)
process.env["NODE_APP_INSTANCE"] = imageType;
const config = require('config')
console.log('Using configuration', config.util.getEnv('NODE_ENV'), 'for instance',process.env["NODE_APP_INSTANCE"], JSON.stringify(config, null, 2));
if (config.app.catchErrors) {
process.on('uncaughtException', (err) => {
console.log("#ERROR# UncaughtException: " + err)
})
}
const watcherRequired = cmds.includes("watcher")
const webserverRequired = cmds.includes("webserver")
const apiserverRequired = cmds.includes("apiserver")
......@@ -45,20 +50,14 @@ function main() {
models = require('./app/models')(mongoose, config);
}
if (cmds.includes("watcher")) {
const fileWatcher = require('./app/filesWatcher')(env, config, models);
const fileWatcher = require('./app/filesWatcher')(config, models);
}
if (cmds.includes("webserver") || cmds.includes("apiserver")) {
const webServer = require('./app/webserver')(env, config, models, cmds);
const webServer = require('./app/webserver')(config, models, cmds);
}
if (cmds.length == 0) {
console.log(`missin command:\n${usage}`)
}
}
if (config.app.catchErrors) {
process.on('uncaughtException', (err) => {
console.log("#ERROR# UncaughtException: " + err)
})
}
main();
......@@ -7,41 +7,40 @@ const k8 = require('./kubernetes');
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){
fs.mkdir(path, parseInt('2775', 8), (err) => {
if(err) throw err;
fs.chown(path, 1000, 1000, (err) => {
if (err)
console.log('Dir '+ path + ' created, error in chown: ' + JSON.stringify(err));
else
console.log('Dir correctly created:' + path);
next();
});
function guaranteeDir(path, next) {
fs.access(path, fs.constants.F_OK | fs.constants.R_OK, (err) => {
if(err){
fs.mkdir(path, parseInt('2775', 8), (err) => {
if(err) throw err;
fs.chown(path, 1000, 1000, (err) => {
if (err)
console.log('Dir '+ path + ' created, error in chown: ' + JSON.stringify(err));
else
console.log('Dir correctly created:' + path);
next();
});
} else {
next();
}
});
}
function guaranteeUserDir(userID, next) {
//Async version needs to be tested thorougly
guaranteeDir(config.userInfo.sharedDir + '/' + userID, function() {
guaranteeDir(config.userInfo.privateDir + '/' + userID, function() {
next();
});
} else {
next();
}
});
}
function guaranteeUserDir(userID, next) {
//Async version needs to be tested thorougly
guaranteeDir(config.userInfo.sharedDir + '/' + userID, function() {
guaranteeDir(config.userInfo.privateDir + '/' + userID, function() {
next();
});
}
});
}
/// functions that either gives the running pod or starts it
function getOrCreatePod(podName, next) {
k8.ns(config.k8component.namespace).pod.get(podName, function(err, result) {
if(err) {
const info = components.infoForPodName(podName)
components.templateForImageType(info.imageType, info.user, info.shortSession, {}, function(err, template, repl) {
/// functions that either gives the running pod or starts it
function getOrCreatePod(podName, repl, shouldCreate, next) {
k8.ns(config.k8component.namespace).pod.get(podName, function(err, result) {
if(err) {
if (shouldCreate) {
components.templateForImage(repl, function(err, template, repl) {
if(err) {
console.log(`#ERROR# Cannot start pod ${podName}, error in template generation: ${JSON.stringify(err)}`);
next(err, null)
......@@ -62,24 +61,29 @@ const reloadMsg = `<html><head><title>Starting up!</title><meta http-equiv="refr
}
});
} else {
console.log(`looked up ${podName}: ${JSON.stringify(result)}`)
next(null, result)
}
});
}
// cache pod name -> host & port
const resolveCache = require('../safe-memory-cache/map.js')({
limit: config.resolveCacheNMax,
maxTTL: config.resolveCacheTtlMaxMs,
refreshF: function(key, value, cache) {
console.log(`#ERROR# requested pod ${podName} which does not exist and should not be created, error: ${JSON.stringify(err)}`);
next(err, null)
}
} else {
console.log(`looked up ${podName}: ${JSON.stringify(result)}`)
next(null, result)
}
})
});
}
// cache pod name -> host & port
const resolveCache = require('../safe-memory-cache/map.js')({
limit: config.resolveCacheNMax,
maxTTL: config.resolveCacheTtlMaxMs,
refreshF: function(key, value, cache) {
}
})
function resolvePod(podName, next) {
function resolvePod(repl, next) {
const podName = components.podNameForRepl(repl)
var v = resolveCache.get(podName)
if (v === undefined) {
getOrCreatePod(podName, function (err, pod) {
getOrCreatePod(podName, repl, config.k8component.image.autoRestart, function (err, pod) {
if (err) {
next(err, null)
} else {
......@@ -103,23 +107,26 @@ const reloadMsg = `<html><head><title>Starting up!</title><meta http-equiv="refr
throw "ProxyRouter backend required. Please provide options.backend parameter!";
}
this.client = options.backend;
};
// The decision could be made using the state machine instead of the
ProxyRouter.prototype.lookup = function(req, res, userID, isWebsocket, path, next) {
console.log("Looking up the path! " + req.path)
if (!req.session.shortSession)
req.session.shortSession = components.shortSession(req.sessionID)
const shortSession = req.session.shortSession
const podName = components.podNameForImageType(config.k8component.imageType, userID, shortSession)
resolvePod(podName, function (err, target) {
if (err) {
console.log(`ERROR ${JSON.stringify(err)}`)
} else {
console.log(`Resolved to ${stringify(target)}`)
next(target);
}
})
};
}
;
// The decision could be made using the state machine instead of the
ProxyRouter.prototype.lookup = function(req, res, userID, isWebsocket, path, next) {
console.log("Looking up the path! " + req.path)
components.cachedReplacements(req, function(err, repl) {
if (err) {
console.log(`ERROR no replacements: lookup without visiting the entry point ${config.k8component.entryPoint.path} (${JSON.stringify(err)})`)
} else {
const podName = components.podNameForImageType(userID, shortSession)
resolvePod(podName, function (err, target) {
if (err) {
console.log(`ERROR ${JSON.stringify(err)}`)
} else {
console.log(`Resolved to ${stringify(target)}`)
next(target);
}
})
}
})
}
module.exports = ProxyRouter
......@@ -6,15 +6,25 @@ const baseDir = path.resolve(__dirname, '..')
const cconfig = config.k8component
const userSettings = require('./userSettings')
const crypto = require('crypto');
const url = require('url');
var baseRepl = {
baseDir: baseDir
baseDir: baseDir,
baseUri: config.app.baseUri,
baseUriPath: url.parse(config.app.baseUri).path
};
for (k in config.app.baseReplacements)
repl[k] = baseRepl[k];
const templatesDir = handlebars.compile(config.app.templatesDir)(baseRepl)
// Create a template from the given string
function templatize(str) {
return handlebars.compile(str)
}
const templatesDir = templatize(config.app.templatesDir)(baseRepl)
baseRepl['templatesDir'] = templatesDir
baseRepl['namespace'] = cconfig.namespace
baseRepl['commands'] = templatize(cconfig.commands.path)(baseRepl)
// Given a path loads it and compiles a template for it, use loadTemplate that has caching
function loadTemplateInternal(templatePath, next) {
......@@ -24,7 +34,7 @@ function loadTemplateInternal(templatePath, next) {
err.message = err.message + ` loading ${templateRealPath}`
next(err,null)
} else {
const template = handlebars.compile(data)
const template = templatize(data)
next(null, template)
}
})
......@@ -63,11 +73,12 @@ function loadTemplate(templatePath, next) {
}
}
// evaluates a template
// evaluates a template, here only the most basic replacements are given, you normally need to pass in extraRepl.
// calls next with the resolved template plus all replacements defined
function evalTemplate(templatePath, extraRepl, next) {
loadTemplate(templatePath, function (err, template) {
if (err) {
next(err, null)
next(err, null, undefined)
} else {
const repl = Object.assign({}, extraRepl, baseRepl)
const res = template(repl)
......@@ -85,41 +96,36 @@ function namespaceTemplate(name, next) {
function shortSession(sessionID) {
const hash = crypto.createHash('sha512');
hash.update(req.sessionID)
return hash.digest('base64').slice(0,14).replace('+','-').replace('/','_')
// lowercase base32 would be better...
return hash.digest('hex').slice(0,20).toLowerCase()
}
/// returns the name of the pod for the give user/session
function podNameForImageType(imageType, user, shortSession) {
var session = cconfig.images[imageType].containerPerSession
if (session !== true && session !== false)
session = cconfig.containerPerSession
if (session)
return `${imageType}-${user}-${shortSession}`
else
return`${imageType}-${user}`
/// returns the name of the pod for the given replacements
function podNameForRepl(repl) {
const imageType = cconfig.image.name
const itypeRe = /^[a-z0-9]+$/
if (!itypeRe.test(imageType))
throw `imageType ${imageType} is invalid (not just lower case letters an numbers)`
if (!itypeRe.test(repl.imageSubtype))
throw `imageSubtype ${repl.imageSubtype} is invalid (not just lower case letters an numbers)`
return `${imageType}-${repl.user}-${repl.imageSubtype}`
}
/// returns the keys (user,...) for the given pod name
function infoForPodName(podName) {
const imageType = podName.slice(0,podName.indexOf('-'))
var session = cconfig.images[imageType].containerPerSession
if (session !== true && session !== false)
session = cconfig.containerPerSession
if (session)
return {
imageType: imageType,
user: podName.slice(imageType.length + 1, podName.length - 15),
shortSession: podName.slice(podName.length - 14)
}
else
return {
imageType: imageType,
user: podName.slice(imageType.length + 1)
}
const imageSubtype = podName.slice(podName.lastIndexOf('-'), podName.length)
const user = podName.slice(imageType.length + 1, podName.length - imageSubtype.length - 1)
return {
imageType: imageType,
user: user,
imageSubtype: imageSubtype
}
}
/// gives the replacements for the image type and user
function replacementsForImageType(imageType, user, shortSession, extraRepl, next) {
/// gives the replacements for the user
function replacementsForUser(user, extraRepl, next) {
var repl = {}
var keysToProtect = new Set()
var toSkip
......@@ -136,7 +142,8 @@ function replacementsForImageType(imageType, user, shortSession, extraRepl, next
keysToProtect.add(k)
}
addRepl(cconfig)
addRepl(cconfig.images[imageType])
addRepl(cconfig.image)
let imageType = cconfig.image.imageType
const userRepl = userSettings.getAppSetting(user, 'image:' + imageType)
addRepl(userRepl)
// extraRepl overrides even protected values
......@@ -146,27 +153,52 @@ function replacementsForImageType(imageType, user, shortSession, extraRepl, next
// "real" user imageType and podName overrided everything
repl['user'] = user
repl['imageType'] = imageType
repl['shortSession'] = shortSession
repl['podName'] = podNameForImageType(imageType, user, shortSession)
repl['podName'] = podNameForRepl(repl)
next(null, repl)
}
function templateForImageType(imageType, user, shortSession, extraRepl, next) {
replacementsForImageType(imageType, user, shortSession, extraRepl, function(err, repl) {
if (err)
next(err, null, null)
else
evalTemplate(repl['templatePath'], repl, next)
})
// returns the name of the logged in user
function selfUserName(req) {
var selfName;
try {
selfName = req.user.id;
} catch(e) {
selfName = ''
}
return selfName
}
// returns the cached replacements if available, creating them if the entryPoint is not exclusive
function cachedReplacements(req, next) {
let imageType = cconfig.image.imageType
var repl = req.session.replacements[imageType]
if (repl) {
next(null, repl)
} else if (!cconfig.entryPoint.exclusiveStartPoint) {
replacementsForUser(selfUserName(req), {}, function(err, newRepl) {
req.session.replacements[imageType] = newRepl
next(null, newRepl)
})
} else {
next({ message: `no replacements defined, you need to visit first the entry point ${cconf.entryPoint.path}` }, undefined)
}
}
function templateForImage(repl, next) {
evalTemplate(repl['templatePath'], repl, next)
}
module.exports = {
baseRepl: baseRepl,
templatize: templatize,
evalTemplate: evalTemplate,
namespaceTemplate: namespaceTemplate,
shortSession: shortSession,
replacementsForImageType: replacementsForImageType,
podNameForImageType: podNameForImageType,
replacementsForUser: replacementsForUser,
selfUserName: selfUserName,
cachedReplacements: cachedReplacements,
podNameForRepl: podNameForRepl,
infoForPodName: infoForPodName,
templateForImageType: templateForImageType
templateForImage: templateForImage
}
'use strict';
module.exports = function(env, config, models){
module.exports = function(config, models){
const chokidar = require('chokidar');
const pathModule = require('path');
const fs = require('fs');
......
'use strict';
module.exports = function(env,config, models, cmds) {
module.exports = function(config, models, cmds) {
const express = require('express');
const http = require('http');
......@@ -28,6 +28,7 @@ module.exports = function(env,config, models, cmds) {
var app = express();
const env = process.env["NODE_ENV"] || 'development'
if (env === 'development') {
// only use in development
app.use(errorHandler())
......
k8component: {
image: {
imageType: beaker
imageSubtype: default1
keysToProtect: ["imageType","containerPerSession"]
image: "labdev-nomad.esc.rzg.mpg.de:5000/nomadlab/notebook:v1.11.5-7-g851b73b-dirty",
port: 8801,
prefix: "/beaker",
homePath: "/home/beaker"
}
}
k8component: {
image: {
imageType: creedo
imageSubtype: default1
keysToProtect: ["containerPerSession"]
image: "labdev-nomad.esc.rzg.mpg.de:5000/nomadlab/creedo:v0.4.2-2017-09-29",
port: 8080,
prefix: "/Creedo",
homePath: "/home/creedo"
}
}
k8component: {
image: {
name: jupyter
subtype: default1
keysToProtect: ["containerPerSession"]
image: "labdev-nomad.esc.rzg.mpg.de:5000/nomadlab/notebook-jupyter-libatoms-tutorial:v0.4",
port: 8888,
prefix: "/jupyter",
homePath: "/home/beaker"
}
}
k8component: {
image: {
name: remotevis
subtype: default1
keysToProtect: ["containerPerSession"]
image: "labdev-nomad.esc.rzg.mpg.de:5000/nomadlab/notebook-jupyter-libatoms-tutorial",
port: 8809,
prefix: "/jupyter",
homePath: "/home/beaker"
templatePath: "remoteVisTemplate.yaml"
}
}
......@@ -48,39 +48,26 @@ k8component: {
namespace: "analytics"
templatePath: "defaultTemplate.yaml"
keysToProtect: ["keysToProtect", "keysToSkip"]
keysToSkip: ["templateCacheTtlMaxMs", "templateCacheNMax", "keysToSkip", "keysToProtect", "images"]
keysToSkip: ["templateCacheTtlMaxMs", "templateCacheNMax", "keysToSkip", "keysToProtect", "images", "entryPoint", "commands"]
containerPerSession: false
imageType: beaker
images: {
beaker: {
keysToProtect: ["containerPerSession"]
image: "labdev-nomad.esc.rzg.mpg.de:5000/nomadlab/notebook:v1.8.0-214-gdd60aa28-dirty",
port: 8801,
prefix: "/beaker",
homePath: "/home/beaker"
},
jupyter: {
keysToProtect: ["containerPerSession"]
image: "labdev-nomad.esc.rzg.mpg.de:5000/nomadlab/notebook-jupyter-libatoms-tutorial:v0.4",
port: 8888,
prefix: "/jupyter",
homePath: "/home/beaker"
},
creedo: {
keysToProtect: ["containerPerSession"]
image: "labdev-nomad.esc.rzg.mpg.de:5000/nomadlab/creedo:v0.4.2-2017-09-29",
port: 8080,
prefix: "/Creedo",
homePath: "/home/creedo"
},
remotevis: {
keysToProtect: ["containerPerSession"]
image: "labdev-nomad.esc.rzg.mpg.de:5000/nomadlab/notebook-jupyter-libatoms-tutorial",
port: 8809,
prefix: "/jupyter",
homePath: "/home/beaker"
templatePath: "remoteVisTemplate.yaml"
}
image: {
imageType: beaker
imageSubtype: default
keysToProtect: ["imageType", "containerPerSession"]
image: "labdev-nomad.esc.rzg.mpg.de:5000/nomadlab/notebook:v1.8.0-214-gdd60aa28-dirty"
port: 8801
prefix: "/beaker"
homePath: "/home/beaker"
autoRestart: true
}
entryPoint: {
path: "/notebook-edit/*"
uriRe: "/notebook-edit(/.*)"
redirectTarget: "{{baseUri}}/beaker/#/open?uri={{escapedPath1}}"
exclusiveStartPoint: false
}
commands: {
path: "{{baseUriPath}}/nomad-commands"
}
}
userInfo: {
......
module.exports = function (app, redirect, config, proxyServer, proxyRouter, k8, passport, fs, ensureLoggedIn, bodyParser) {
function makeid(){
var text = "";
var possible = "abcdefghijklmnopqrstuvwxyz0123456789";
for( var i=0; i < 5; i++ )
text += possible.charAt(Math.floor(Math.random() * possible.length));
return text;
}
const components = require('../app/components')
function setFrontendHeader() {
return function(req, res, next) {
res.setHeader('Access-Control-Allow-Origin', config.app.frontendAddr);
......@@ -21,24 +15,57 @@ module.exports = function (app, redirect, config, proxyServer, proxyRouter, k8,
res.setHeader('content-type', 'application/vnd.api+json');
next();
}
}
// returns the name of the logget in user
function selfUserName(req) {
var selfName;
try {
selfName = req.user.id;
} catch(e) {
selfName = ''
}
return selfName
}
}
app.get('/nmdalive', function(req, res){
res.send("<div>Hello2!!</div>");
});
let cconf = config.k8component
app.get(cconf.entryPoint.path, ensureLoggedIn('/login'), function(req, res){
extraArgs = object.create(req.query)
extraArgs.path = req.url
if (cconf.entryPoint.pathReStr) {
let re = new RegExp(cconf.entryPoint.pathReStr)
var iRe = 1
while (undefined !== re[iRe]) {
let iReStr = iRe.toString()
let pVal = re[iRe]
extraArgs["path" + iReStr] = pVal
extraArgs["escapedPath" + iReStr] = pVal.replace("/","%2F")
}
}
let user = components.selfUserName(req)
components.replacementsForUser(user, extraArgs, function(err, repl) {
if (!err) {