Commit 189893bd authored by Félix Hamel's avatar Félix Hamel

1.7.25-pre

parent 4b487324
......@@ -16,14 +16,13 @@
See the License for the specific language governing permissions and
limitations under the License.
*/
var P = require("bluebird"),
bunyan = require("bunyan"),
Hapi = require("hapi"),
bformat = require("bunyan-format"),
Injector = require("./injector"),
_ = require("lodash"),
os = require("os"),
timber = require("timber");
const P = require("bluebird");
const bunyan = require("bunyan");
const Hapi = require("hapi");
const bformat = require("bunyan-format");
const Injector = require("./injector");
const _ = require("lodash");
const timber = require("timber");
// Prepare logs
var streams = [
......@@ -37,94 +36,165 @@ var streams = [
if (typeof process.env.TIMBER_KEY !== "undefined") {
var transport = new timber.transports.HTTPS(process.env.TIMBER_KEY);
timber.install(transport);
streams.push({
name: process.env.TIMBER_NAME || "core",
level: process.env.TIMBER_LEVEL || "info",
level: process.env.TIMBER_LEVEL || "debug",
stream: new timber.transports.Bunyan()
});
}
function CMBF() {
// Prepare CMBF
this.plugins = [];
this.hooks = {};
this.injector = new Injector();
this.log = bunyan.createLogger({
name: "core",
level: process.env.SYSTEM_LOG_LEVEL || "info",
streams: streams
});
if (process.env.NODE_ENV !== "production") {
this.log.warn(
"Enabling Bluebird long stack trace in development and samples mode"
);
P.longStackTraces();
function printDependencies(deps) {
if (_.isEmpty(deps)) {
return "0 plugins";
}
else {
return _.reduce(deps, function(item) {
return item + ", ";
});
}
}
CMBF.prototype.registerHook = function(key, hookHandler) {
this.log.debug("Registering hook %s", key);
this.hooks[key] = P.method(hookHandler);
};
class CMBF {
constructor() {
// Prepare CMBF
this.plugins = [];
this.hooks = {};
this.injector = new Injector();
this.log = bunyan.createLogger({
name: "core",
level: process.env.SYSTEM_LOG_LEVEL || "info",
streams: streams
});
CMBF.prototype.registerHooks = function(hooks) {
var _this = this;
return _.each(_.keys(hooks), function(hookKey) {
_this.registerHook(hookKey, hooks[hookKey]);
});
};
// Enable debug mode
if (process.env.NODE_ENV !== "production") {
this.log.warn("Enabling Bluebird long stack trace in development and samples mode");
P.longStackTraces();
}
}
/**
* Generic hook calling helper. All hooks are promised-wrapped. DefaultImpl will be called
* if no hook is present and will be passed to hooks for default implementation chaining.
*
* @param key
* @param params
* @param defaultImpl
* @returns {*}
*/
CMBF.prototype.callHook = function(key, params, defaultImpl) {
this.log.debug("Calling hook %s", key);
registerHook(key, hookHandler) {
this.log.debug("Registering hook %s", key);
this.hooks[key] = P.method(hookHandler);
}
if (arguments.length === 2 && _.isFunction(params)) {
defaultImpl = params;
params = {};
registerHooks(hooks) {
return _.each(_.keys(hooks), hookKey => this.registerHook(hookKey, hooks[hookKey]));
}
var hook = this.hooks[key];
if (hook) {
this.log.trace("Registered hook %s will be used", key);
return P.method(hook).call(this, params, defaultImpl);
/**
* Generic hook calling helper. All hooks are promised-wrapped. DefaultImpl will be called
* if no hook is present and will be passed to hooks for default implementation chaining.
*
* @param key
* @param params
* @param defaultImpl
* @returns {*}
*/
callHook(key, params, defaultImpl) {
this.log.debug("Calling hook %s", key);
if (arguments.length === 2 && _.isFunction(params)) {
defaultImpl = params;
params = {};
}
let hook = this.hooks[key];
if (hook) {
this.log.trace("Registered hook %s will be used", key);
return P.method(hook).call(this, params, defaultImpl);
}
else if (defaultImpl) {
this.log.trace("No registered hook %s was found but a default implementation was provided", key);
return P.method(defaultImpl)(params);
}
else {
this.log.trace("No hook found, let's continue");
return P.resolve(params);
}
}
else if (defaultImpl) {
this.log.trace(
"No registered hook %s was found but a default implementation was provided",
key
/**
* Wait for deps to be loaded
* @param deps
* @param options
* @return {Promise<T | never>}
*/
waitFor(deps, options) {
options = options || { timeout: 0 };
// Make sure each entry is pre-created
let depsResolvers = P.map(deps, (dep) => {
// Record our entry if not already in there (someone maybe already waiting for us)
let entry = _.find(this.plugins, p => p.name === dep);
if (!entry) {
entry = {
name: dep,
resolver: {}
};
entry.resolver.promise = new P(function() {
entry.resolver.resolve = arguments[0];
entry.resolver.reject = arguments[1];
});
this.plugins.push(entry);
}
return entry.resolver.promise;
});
this.log.info(
"Plugin %s is waiting for %s [Timeout: %s ms] ",
options.debug,
printDependencies(deps),
options.timeout
);
return P.method(defaultImpl)(params);
}
else {
this.log.trace("No hook found, let's continue");
return P.resolve(params);
return P.all(depsResolvers)
.timeout(options.timeout)
.then(() => {
this.log.info(
"All dependencies for plugin %s were successfully loaded",
options.debug
);
})
.catch((error) => {
this.log.error(
"Unable to load plugin %s. Was waiting for deps %s [Error: %s]",
options.debug,
printDependencies(deps),
error
);
});
}
};
CMBF.prototype.waitFor = function(deps, options) {
var _this = this;
options = options || { timeout: 0 };
/**
* Install plugin on server
* @param plugin
* @param options
* @return {Promise<*>}
*/
async installPlugin(plugin, options) {
options = options || {};
let path = plugin.register.attributes.pkg.name;
this.log.debug("Installing plugin %s", path);
// Make sure each entry is pre-created
var depsResolvers = P.map(deps, function(dep) {
// Record our entry if not already in there (someone maybe already waiting for us)
var entry = _.find(_this.plugins, function(p) {
return p.name === dep;
let entry = _.find(this.plugins, (p) => {
return p.name === plugin.register.attributes.pkg.name;
});
if (!entry) {
this.log.trace(
"Plugin %s does not exist, let's pre-created the entry",
path
);
entry = {
name: dep,
name: plugin.register.attributes.pkg.name,
version: plugin.register.attributes.pkg.version,
plugin: plugin,
options: options,
resolver: {}
};
......@@ -133,474 +203,447 @@ CMBF.prototype.waitFor = function(deps, options) {
entry.resolver.reject = arguments[1];
});
_this.plugins.push(entry);
this.plugins.push(entry);
}
else {
this.log.trace(
"Plugin %s was already pre-registered. Updating implementation and version only",
path
);
entry.version = plugin.register.attributes.pkg.version;
entry.plugin = plugin;
}
return entry.resolver.promise;
});
// Wait for any dependencies and register the plugin
try {
await this.waitFor(plugin.deps || options.deps || [], {
debug: entry.name,
timeout: process.env.COVISTRA_LOAD_PLUGIN_TIMEOUT || 15000
});
_this.log.info(
"Plugin %s is waiting for %s [Timeout: %s ms] ",
options.debug,
printDependencies(deps),
options.timeout
);
return P.all(depsResolvers)
.timeout(options.timeout)
.then(function() {
_this.log.info(
"All dependencies for plugin %s were successfully loaded",
options.debug
);
})
.catch(function(error) {
_this.log.error(
"Unable to load plugin %s. Was waiting for deps %s [Error: %s]",
options.debug,
printDependencies(deps),
error
this.log.trace(
"Plugin %s dependencies are all resolved",
entry.name,
entry.options
);
});
};
await this.server.register({
register: entry.plugin,
options: entry.options.runtime
});
this.log.trace("Indicating plugin %s has resolved", path);
return entry.resolver.resolve();
}
catch (err) {
this.log.error("Error while registering plugin %s", path, err);
return entry.resolver.reject(err);
}
function printDependencies(deps) {
if (_.isEmpty(deps)) {
return "0 plugins";
}
else {
return _.reduce(deps, function(item) {
return item + ", ";
});
}
}
CMBF.prototype.installPlugin = P.method(function(plugin, options) {
var _this = this;
options = options || {};
async launch(pluginOpts) {
var path = plugin.register.attributes.pkg.name;
_this.log.debug("Installing plugin %s", path);
this.log.info("Initiating the CMBF launch process");
// Record our entry if not already in there (someone maybe already waiting for us)
var entry = _.find(this.plugins, function(p) {
return p.name === plugin.register.attributes.pkg.name;
});
if (!entry) {
_this.log.trace(
"Plugin %s does not exist, let's pre-created the entry",
path
// Configure the server
const SERVER_CONFIG = this.serverConfiguration;
let opts = await this.callHook("configure-server", SERVER_CONFIG);
// Create Hapi server instance
this.log.trace("Server configuration will be", opts);
this.server = new Hapi.Server(opts);
this.server.decorate("server", "cmbf", this);
this.server.method(
"getCmbf",
() => this,
{}
);
entry = {
name: plugin.register.attributes.pkg.name,
version: plugin.register.attributes.pkg.version,
plugin: plugin,
options: options,
resolver: {}
// Register early configuration
await this._registerEarlyConfiguration(pluginOpts);
this.server.decorate("server", "config", this.appConfiguration);
await this._injectDependencies();
await this._registerConnections();
await this._registerSecurity(pluginOpts);
await this._registerSwagger(pluginOpts);
await this._registerPlugins(pluginOpts);
await this._registerInAndOutLogs();
await this._registerLatePlugins();
// Perform pre-start global configuration
this.log.debug("All plugins are loaded and ready. Performing pre-start logic");
await this.callHook("before-server-start", { server: this.server });
// Launch the server
this.log.info("Starting the server");
await this.server.start();
this.log.info("Server started !");
await this.callHook("server-started", { server: this.server });
// Launch is complete
this.log.info("Server was completely started!");
return this;
}
/**
* Get the server configuration
* @return {{debug: {request: string[]}, cache: object[]}}
*/
get serverConfiguration() {
// Initializing the Hapi server
let serverOpts = {
debug: { request: ["error", "warning"] }
};
entry.resolver.promise = new P(function() {
entry.resolver.resolve = arguments[0];
entry.resolver.reject = arguments[1];
});
// Add cache config
const cacheConfig = this.cacheConfiguration;
if (_.size(cacheConfig) > 0) {
serverOpts.cache = cacheConfig;
}
this.plugins.push(entry);
return serverOpts;
}
else {
_this.log.trace(
"Plugin %s was already pre-registered. Updating implementation and version only",
path
/**
* Retrieve the cache configuration
* @return {Array}
*/
get cacheConfiguration() {
let caches = [];
// Fhamel : Removed MongoDB cache (useless)
if (process.env.CACHE_REDIS_HOST) {
caches.push({
name: "redisCache",
engine: require("catbox-redis"),
host: process.env.CACHE_REDIS_HOST,
port: process.env.CACHE_REDIS_PORT,
password: process.env.CACHE_REDIS_PASSWORD,
database: process.env.CACHE_REDIS_DATABASE,
partition: "cache"
});
}
return caches;
}
/**
* Return the computed configuration (config file)
* @return {*}
*/
get appConfiguration() {
return this.server.plugins["hapi-config"].CurrentConfiguration;
}
/**
*
* @param pluginOpts
* @return {Promise<*>}
* @private
*/
async _registerEarlyConfiguration(pluginOpts) {
// Register early plugins (default load config)
return this.callHook(
"register-early-plugins",
{ server: this.server, pluginOptions: pluginOpts },
() => {
this.log.debug("Register Early Plugin: hapi-config");
return this.server.register({
register: require("hapi-config"),
options: pluginOpts || {}
});
}
);
entry.version = plugin.register.attributes.pkg.version;
entry.plugin = plugin;
}
// Wait for any dependencies and register the plugin
return this.waitFor(plugin.deps || options.deps || [], {
debug: entry.name,
timeout: 15000
})
.then(function() {
_this.log.trace(
"Plugin %s dependencies are all resolved",
entry.name,
entry.options
/**
*
* @return {Promise<*>}
* @private
*/
async _injectDependencies() {
// Register all required dependencies
this.log.debug("Injecting dependencies");
return this.callHook("inject-dependencies", { injector: this.injector });
}
/**
*
* @return {Promise<*>}
* @private
*/
async _registerConnections() {
const config = this.appConfiguration;
if (!config) {
throw new Error("Missing hapi-config plugin. Must be loaded in the register-early-plugin hook");
}
this.log.debug("Configuring server connections");
await this.callHook("register-connections", { server: this.server, config: config });
const API_OPTS = {
port: config.get("PORT"),
labels: ["api"],
router: {
stripTrailingSlash: true
},
routes: {
log: true,
cors: {
additionalHeaders: ["X-App-Key", "X-Unsafe-Auth"],
credentials: true
},
payload: {
maxBytes: 1024 * 1024 * 1024 * 64
}
}
};
let connections = [];
// Serve admin through the same connection if no specific ADMIN_PORT is defined
if (config.get("ADMIN_PORT")) {
this.log.debug(
"Detected a separate ADMIN_PORT %d. Configure a separate server connection",
config.get("ADMIN_PORT")
);
return P.promisify(_this.server.register, _this.server)({
register: entry.plugin,
options: entry.options.runtime
})
.then(function() {
_this.log.trace("Indicating plugin %s has resolved", path);
entry.resolver.resolve();
})
.catch(function(err) {
_this.log.error("Unable to register plugin %s. ", path, err);
entry.resolver.reject(err);
});
})
.catch(function(err) {
_this.log.error("Error while registering plugin %s", path, err);
connections.push(API_OPTS);
connections.push({
port: config.get("ADMIN_PORT"),
labels: ["admin"],
router: {
stripTrailingSlash: true
},
routes: {
payload: {
maxBytes: 1024 * 1024 * 1024 * 64
}
}
});
}
else {
API_OPTS.labels.push("admin");
connections.push(API_OPTS);
}
// Configure connections
let result = await this.callHook("configure-connections", {
config: config,
server: this.server,
connections: connections
});
});
CMBF.prototype.launch = P.method(function(pluginOpts) {
var _this = this;
this.log.info("Initiating the CMBF launch process");
var caches = [];
if (process.env.CACHE_REDIS_HOST) {
caches.push({
name: "redisCache",
engine: require("catbox-redis"),
host: process.env.CACHE_REDIS_HOST,
port: process.env.CACHE_REDIS_PORT,
password: process.env.CACHE_REDIS_PASSWORD,
database: process.env.CACHE_REDIS_DATABASE,
partition: "cache"
this.log.debug("%d connections will be configured for our server", result.connections.length);
return P.each(result.connections, (connectionOpts) => {
this.server.connection(connectionOpts);
});
}
// Initializing the Hapi server
var serverOpts = {
debug: {
request: ["error", "warning"]
}
};
if (_.size(caches) > 0) {
serverOpts.cache = caches;
/**
*
* @param pluginOpts
* @return {Promise<*>}
* @private
*/
async _registerSecurity(pluginOpts) {
// Register security plugins (auth strategies)
this.log.debug("Configure security plugin strategies");
return this.callHook("configure-security", {
server: this.server,
config: this.appConfiguration,
pluginOptions: pluginOpts
}, async () => {
this.log.debug(
"Configure Security: Loading 3 auth strategies: hapi-auth-bearer-token, hapi-auth-cookie, hapi-auth-basic"
);
return P.join(
this.server.register({
register: require("hapi-auth-bearer-token"),
options: pluginOpts
}),
this.server.register({
register: require("hapi-auth-cookie"),
options: pluginOpts
}),
this.server.register({
register: require("hapi-auth-basic"),
options: pluginOpts
})
);
});
}
// Configure the server
return _this.callHook("configure-server", serverOpts).then(function(opts) {
_this.log.trace("Server configuration will be", opts);
var server = new Hapi.Server(opts);
/**
* Register the Swagger plugin
* @param pluginOpts
* @return {Promise<*>}
* @private
*/
async _registerSwagger(pluginOpts) {
this.log.debug("Configuring API documentation plugin (Swagger)");
const config = this.appConfiguration;
const swaggerOpts = {
schemes: [config.get("PROTOCOL", "http")],
host: config.get("HOST", "localhost:5000"),
info: config.get("server:info"),
tags: config.get("server.tags")
};
const configureDocumentationResult = await this.callHook("configure-documentation", {
server: this.server,
config: config,
swaggerOpts: swaggerOpts
});
_this.server = server;
server.decorate("server", "cmbf", _this);
server.method(
"getCmbf",
function() {
return _this;
},
{}
this.log.debug(
"Configuring Documentation: Swagger plugin will be loaded with configuration",
configureDocumentationResult.swaggerOpts
);
return P.join(
this.server.register({
register: require("inert"),
options: _.defaults(
config.get("plugins:inert:options", {}),
pluginOpts
)
}),
this.server.register({
register: require("vision"),
options: _.defaults(
config.get("plugins:vision:options", {}),
pluginOpts
)
}),
this.server.register({
register: require("hapi-swagger"),
options: configureDocumentationResult.swaggerOpts
}),
this.server.register({
register: require("chairo"),
options: _.defaults(
config.get("plugins:chairo:options", {}),
pluginOpts
)
})
);
// Register early plugins (default load config)
return (_this
.callHook(
"register-early-plugins",
{ server: server, pluginOptions: pluginOpts },
function() {
_this.log.debug("Register Early Plugin: hapi-config");
var registerPlugin = P.promisify(server.register, server);
return P.join(
registerPlugin({
register: require("hapi-config"),
options: pluginOpts || {}
})
);
}
)
// Register all connections
.then(function() {
// Retrieve the computed configuration
var config = server.plugins["hapi-config"].CurrentConfiguration;