| Server IP : 34.87.99.72 / Your IP : 216.73.217.31 Web Server : Apache/2.4.46 (Unix) OpenSSL/1.1.1n System : Linux zlock-bitnami-lamp-prod-v2-vm 4.19.0-16-cloud-amd64 #1 SMP Debian 4.19.181-1 (2021-03-19) x86_64 User : bitnami ( 1000) PHP Version : 7.4.18 Disable Function : NONE MySQL : OFF | cURL : ON | WGET : ON | Perl : ON | Python : ON | Sudo : ON | Pkexec : OFF Directory : /opt/bitnami/nami/cli/ |
Upload File : |
'use strict';
const path = require('path');
const fs = require('fs');
const Parser = require('./nami-parser');
const _ = require('nami-utils/lodash-extra.js');
const Manager = require('nami-core');
const hu = require('nami-utils');
const strftime = require('strftime');
const Logger = require('nami-logger');
const JsonParser = require('nami-core/lib/json-parser.js');
const templates = require('nami-utils/templates');
const pkgData = require('../package.json');
const hpRootDir = path.dirname(path.dirname(fs.realpathSync(__filename)));
const templatesDir = path.join(hpRootDir, 'templates');
const buildTimestamp = path.join(hpRootDir, 'timestamp');
class NamiCliApp {
constructor(options) {
options = _.defaults(options || {}, {logger: null});
this._exitCode = 0;
this.version = pkgData.version;
this.revision = '0';
this.builtOn = '';
try {
this.builtOn = hu.file.read(buildTimestamp).trim();
} catch (e) {
/* not-empty */
}
this.logger = options.logger || new Logger({
logFile: hu.os.getTempFile(strftime('nami_%s.log')),
logFileLevel: 7, prefix: 'nami'
});
hu.delegate(this, ['logToFile', 'info', 'error', 'debug', 'warn', 'trace'], this.logger);
hu.delegate(this, {logLevel: 'level'}, this.logger);
this.parser = new Parser({logger: this.logger, throwOnExit: true, allowProcessExit: false});
// We do a preliminary parsing without the commands
// populated so we can get some basic settings such as the log-level
this.parser.parse(process.argv.slice(2), {
abortOnUnknown: false,
abortOnError: false,
silent: true,
onlyFlags: true
});
const managerConfiguration = _.merge({
logger: this.logger,
encryptionPassword: this.parser.getOptionValue('encryption-password'),
registryPrefix: this.parser.getOptionValue('nami-prefix')
}, this._loadNamiEnvironment({
cfgFile: this.parser.getOptionValue('options-file'),
noProfile: this.parser.getOptionValue('no-profile'),
noRc: this.parser.getOptionValue('no-rc')
}));
this.manager = new Manager(managerConfiguration);
// This is slow and is not yet useful. It is intended to allow providing options
// for multiple installed packages. For example, if my package 'foo' expects
// mysql, the population of global flags will allow passing:
//
// --component.mysql.password=something
//
// We will revisit this and its performance implications once multi-packages setup
// is fully supported
//
// _.each(this.manager.listPackages(), function(pkg) {
// _this.parser.addGlobalFlags(pkg);
// });
this.parser.addOption({
name: 'version', description: 'Nami Version',
allowNegated: false, type: 'boolean', exitAfterCallBack: true,
callback: () => console.log(this.versionString)
});
this._populateCommands();
}
get exitCode() {
return this._exitCode || this.manager.exitCode;
}
set exitCode(value) {
this._exitCode = value;
}
get versionString() {
let text = `${this.version}-${this.revision}`;
if (!_.isEmpty(this.builtOn)) text += ` (${strftime('%F %T', new Date(parseInt(this.builtOn, 10)))})`;
return text;
}
_loadEnvironmentFile(file) {
const config = {};
this.debug(`Loading ${file}`);
if (JsonParser.isJSON(file)) {
const json = JsonParser.parse(file);
_.each({
'installation.prefix': 'installationPrefix',
'encryption.password': 'encryptionPassword',
'serialization.policy': 'serializationPolicy',
'namiPrefix': 'registryPrefix',
'logging.logLevel': 'logLevel'
}, (key, jsonKey) => {
if (_.has(json, jsonKey)) {
this.trace(`Reading key ${jsonKey}`);
config[key] = _.get(json, jsonKey);
}
});
} else {
this.debug('Parsing non-JSON files is still not implemented');
}
return config;
}
_loadNamiEnvironment(options) {
options = _.opts(options, {cfgFile: null, noProfile: false, noRc: false});
const files = [];
if (!options.noProfile) {
files.push('/etc/namirc');
if (!options.noRc) {
files.push('~/.namirc', '~/.nami.json');
}
}
if (options.cfgFile) {
files.push(options.cfgFile);
}
const config = {};
_.each(files, file => {
const normFile = hu.file.normalize(file);
if (hu.file.exists(normFile)) {
_.extend(config, this._loadEnvironmentFile(normFile, options));
}
});
return config;
}
_executePkgCmd(obj, cmd) {
const callOpts = cmd.providedArguments || [];
const cmdOpts = cmd.getFlattenOptions({camelize: true});
callOpts.push(cmdOpts);
return (obj.exports[cmd.name]).apply(obj, callOpts);
}
_processSelectedServiceCmd(obj, cmdName, cmdOpts, appOpts) {
const cmd = obj.parser.getCommand(cmdName);
cmd.parseData(cmdOpts);
if (!_.isEmpty(appOpts)) {
obj.parser.parse(
appOpts,
{abortIfRequiredAndNotProvided: false}
);
}
const result = this._executePkgCmd(obj, cmd);
// If the command did not returner a proper status
// (like when then command is manually overwritten in main.json), we manually ask for it
const status = result || obj.status();
return status;
}
_trapTerminationSignals(obj) {
_.each(['SIGINT', 'SIGTERM'], signal => {
process.on(signal, () => {
try {
obj.stop();
} catch (e) {
this.warn(`The service failed to stop: ${e.message}`);
} finally {
process.exit();
}
});
});
}
_ensureComponentIsInitialized(obj) {
if (obj.lifecycle !== 'installed') {
// We set the exit code to "5 program is not installed",
// as per LSB recommenddation:
// http://refspecs.linuxbase.org/LSB_3.1.0/LSB-Core-generic/LSB-Core-generic/iniscrptact.html
// This is defined for services but we are adopting it for all non-initialized errors
this.exitCode = 5;
throw new Error(`${obj.id} is not fully installed. You cannot execute commands`);
}
}
_reportServiceCmdResult(result) {
console.log(result.msg || result.statusOutput);
}
serviceStart(obj, cmdOpts, appOpts) {
// TODO: This is a hack to make sure we do not break stacksmith containers
// and I hate myself for doing it. The idea is to remove this after we are
// sure the --foreground flag is in its proper place
if (appOpts.length === 1 && appOpts[0] === '--foreground') {
appOpts = [];
cmdOpts.foreground = true;
this.logger.warn(
'Using legacy format for start command, please move the --foreground flag before the service name'
);
}
const result = this._processSelectedServiceCmd(obj, 'start', cmdOpts, appOpts);
if (cmdOpts.foreground) {
this._trapTerminationSignals(obj);
} else {
this._reportServiceCmdResult(result);
}
return result;
}
serviceStop(obj, cmdOpts, appOpts) {
const result = this._processSelectedServiceCmd(obj, 'stop', cmdOpts, appOpts);
console.log(result.msg || result.statusOutput);
return result;
}
serviceStatus(obj, cmdOpts, appOpts) {
const result = this._processSelectedServiceCmd(obj, 'status', cmdOpts, appOpts);
this._reportServiceCmdResult(result);
this.exitCode = result.code;
return result;
}
serviceRestart(obj, cmdOpts, appOpts) {
const result = this._processSelectedServiceCmd(obj, 'restart', cmdOpts, appOpts);
if (cmdOpts.foreground) {
this._trapTerminationSignals(obj);
} else {
this._reportServiceCmdResult(result);
}
return result;
}
listPackagesCmd(/* options */) {
this.debug('Installed Packages');
const res = this.manager.listPackages();
console.log(JSON.stringify(_.keys(res)));
}
execute(pkgName, options) {
const obj = _.first(this.manager.search(pkgName));
this._ensureComponentIsInitialized(obj);
obj.parser.parse(options.args || [], {
abortOnUnknown: false, abortIfRequiredAndNotProvided: false
});
const cmd = obj.parser.selectedCommand;
if (cmd === null) {
return obj.parser.showHelp();
}
this.debug(`Executing ${pkgName} ${cmd.name}`);
return this._executePkgCmd(obj, cmd);
}
_findTemplateFile(resource, options) {
options = _.opts(options, {type: 'component', example: false});
const type = options.type.toLowerCase();
let searchDirs = [type, ''];
// Prepend the same searh paths under the examples top dir
if (options.example) {
searchDirs = _.map(searchDirs, dir => path.join('examples', dir)).concat(searchDirs);
}
searchDirs = _.map(searchDirs, dir => path.join(templatesDir, dir));
const resourceDir = searchDirs.find(dir => hu.file.exists(path.join(dir, resource)));
if (resourceDir) {
const resourceFile = path.join(resourceDir, resource);
this.trace(`'${resource}' found in '${resourceFile}'`);
return resourceFile;
} else {
throw new Error(`Cannot find '${resource}' in any of:\n ${searchDirs.join('\n ')}`);
}
}
scaffold(pkg, options) {
options = _.opts(options, {
name: null, id: null, version: '1.0.0', revision: 0,
type: 'Component', kind: 'template', example: false
});
pkg = hu.file.normalize(pkg);
const name = options.name || options.id || path.basename(pkg);
this.info(`Creating new nami package under ${pkg}`);
if (hu.file.exists(pkg)) {
if (options.force) {
this.warn(`Overwriting files under ${pkg}`);
} else {
throw new Error(`Directory ${pkg} already exists`);
}
}
if (options.kind !== 'template') {
options.example = true;
}
hu.file.mkdir(pkg);
const templateOpts = {
name: name, id: options.id || name, revision: options.revision,
version: options.version
};
_.each({'nami.json.tpl': 'nami.json', 'main.js.tpl': 'main.js'}, (file, template) => {
templates.renderToFile(
this._findTemplateFile(template, options), path.join(pkg, file), templateOpts
);
});
}
_setFailedExitCode() {
// this ensures that we do not override manually set exitCode
if (this.exitCode === 0) {
this.exitCode = 1;
}
}
exposeManagerCmd(cmdOpts, optionsOpts) {
const that = this;
cmdOpts = _.defaults(cmdOpts || {}, {outputHandler: _.noop, minArgs: 1, maxArgs: 1, namedArgs: ['package']});
const managerCmd = cmdOpts.managerCmd || cmdOpts.name;
const outputHandler = cmdOpts.outputHandler;
if (!this.manager[managerCmd]) {
throw new Error(`Cannot find manager command ${managerCmd}`);
}
const cmd = this.manager[managerCmd].bind(this.manager);
const namedArg = cmdOpts.namedArgs[0];
cmdOpts.callback = function() {
try {
const result = cmd(this.arguments[namedArg], _.extend(
this.getFlattenOptions({camelize: true}),
{args: this.extraArgs}
));
outputHandler(result);
} catch (e) {
console.error(e.message);
that.trace(e.stack);
that._setFailedExitCode();
}
};
this.parser.addCommand(cmdOpts, optionsOpts || []);
}
_populateCommands() {
const manager = this.manager;
const parser = this.parser;
const that = this;
this.parser.manager = manager;
parser.addCommand({name: 'list', description: 'List Installed Packages', callback: function() {
that.listPackagesCmd({args: this.extraArgs});
}});
// INSTALL CMD
const commonInstallOpts = [
{
name: 'install-prefix', cliName: 'prefix', alias: 'prefix',
description: 'Installation Prefix', defaultValue: manager.installationPrefix
},
{name: 'force', type: 'boolean', description: 'Force reinstallation', allowNegated: false}
];
this.exposeManagerCmd({
name: 'install',
enableBuiltInHelp: false,
description: 'Install a package in the system. It can be a directory or a zip file.'
}, commonInstallOpts);
// No post installation
this.exposeManagerCmd({
name: 'unpack',
description: 'Install package without performing its post-installation (only unpack files)'
}, commonInstallOpts);
this.exposeManagerCmd({name: 'uninstall', description: 'Uninstall a package'});
parser.addCommand({
name: 'test', description: 'Test an already installed package',
namedArgs: ['pkgName'],
minArgs: 1, maxArgs: 1,
callback: function() {
try {
const options = this.getFlattenOptions({camelize: true});
const exclude = (options.exclude || '').split(';');
const include = (options.include || '*').split(';');
let grep = null;
if (!_.isEmpty(options.grep)) {
try {
grep = new RegExp(options.grep);
} catch (e) {
that.warn('Invalid pattern provided. Ignoring it');
that.trace(e.message);
}
}
manager.test(this.arguments.pkgName, function(failures) {
if (failures > 0) {
process.exit(1);
}
}, {
testDir: options.testDir,
include: include,
exclude: exclude,
grep: grep,
reporter: options.reporter,
fileReporter: options.fileReporter,
reportFile: options.reportFile
});
} catch (e) {
console.log(e.message);
that.trace(e.stack);
that._setFailedExitCode();
}
}
}, [
{name: 'test-dir', description: 'Directory containing the tests to execute'},
{name: 'grep', description: 'Only execute tests matching the provided regular expression', defaultValue: ''},
{
name: 'include',
description: 'Semicolon-separated list of patterns to match against the list of test files for inclusion',
defaultValue: '*'
},
{
name: 'exclude',
description: 'Semicolon-separated list of patterns to match against the list of test files for exclusion',
defaultValue: ''
},
{
name: 'reporter',
defaultValue: 'spec',
description: 'Format used for mocha tests report printed to the standard output',
type: 'choice', validValues: ['spec', 'json', 'xunit'], canBeEmpty: false
},
{
name: 'file-reporter',
defaultValue: 'xunit',
description: 'Format used for mocha tests report printed to a file',
type: 'choice', validValues: ['spec', 'json', 'xunit'], canBeEmpty: true
},
{
name: 'report-file', description: 'File where the tests report will be written'
}
]);
// Only post installation
this.exposeManagerCmd({
name: 'initialize', managerCmd: 'initializePackage',
description: 'Initialized a previously unpacked packaged (run its post-installlation steps)'
}, [
{name: 'force', type: 'boolean', description: 'Force re-initialization of an already initialized package'}
]);
parser.addCommand({
name: 'console', description: 'Opens the interactive nami console',
secret: true, namedArgs: ['pkgName'],
minArgs: 0, maxArgs: 1,
callback: function() {
try {
manager.console(
this.arguments.pkgName,
_.extend(this.getFlattenOptions({camelize: true}), {args: this.extraArgs})
);
} catch (e) {
console.log(e.message);
that.trace(e.stack);
that._setFailedExitCode();
}
}
});
parser.addCommand({
name: 'eval', description: 'Execute file in nami environment', secret: true,
namedArgs: ['file'],
minArgs: 1, maxArgs: 1,
callback: function() {
manager.evalFile(
this.arguments.file,
_.extend(this.getFlattenOptions({camelize: true}), {args: this.extraArgs})
);
}
}, [
{
name: 'package',
defaultValue: null,
description: 'Instead of the global context, invoke in the context of the provided package'
}
]);
parser.addCommand({
name: 'new', description: 'Creates a new nami package template',
minArgs: 1, maxArgs: 1, namedArgs: ['package'],
callback: function() {
that.scaffold(
this.arguments.package, this.getFlattenOptions({camelize: true})
);
}
}, [
{name: 'force', type: 'boolean'},
{name: 'name', defaultValue: null},
{name: 'id', defaultValue: null},
{name: 'version', defaultValue: '1.0.0'},
{
name: 'kind',
defaultValue: 'template',
description: 'Kind of skeleton to use',
type: 'choice',
validValues: ['template', 'detailed', 'full']
},
{
name: 'type',
defaultValue: 'Component',
type: 'choice',
validValues: ['DatabaseService', 'Service', 'Component'],
description: 'Kind of component to package'
}
]);
parser.addCommand({
name: 'execute', description: 'Execute one of the package exported commands',
minArgs: 1, maxArgs: 1, namedArgs: ['package'],
callback: function() {
try {
const result = that.execute(this.arguments.package, _.extend(
this.getFlattenOptions({camelize: true}),
{args: this.extraArgs}
));
console.log(JSON.stringify(result, null, 2));
} catch (e) {
console.log(e.message);
that.trace(e.stack);
that._setFailedExitCode();
}
}
});
const serviceCmds = {};
const serviceMethods = {
start: 'serviceStart', stop: 'serviceStop',
restart: 'serviceRestart', 'status': 'serviceStatus'
};
_.each({
start: 'Start service', stop: 'Stop service',
restart: 'Restart service', status: 'Get service status'
}, function(description, name) {
const serviceMethodName = serviceMethods[name];
const cmd = that[serviceMethodName].bind(that);
serviceCmds[name] = parser.addCommand({
name: name, description: description,
minArgs: 1, maxArgs: 1, namedArgs: ['service'],
callback: function() {
const serviceName = this.arguments.service;
const obj = _.first(manager.search(serviceName));
if (!_.contains(hu.getInheritanceChain(obj), 'Service')) {
throw new Error(`Service commands are only supported for services`);
}
that._ensureComponentIsInitialized(obj);
return cmd(obj, this.getFlattenOptions({camelize: true}), this.extraArgs);
}
});
});
_.each(['start', 'restart'], function(name) {
serviceCmds[name].addOption({name: 'foreground', type: 'boolean'});
});
this.exposeManagerCmd({
name: 'refresh', managerCmd: 'refreshMetadata', secret: true,
desciption: 'Update package metadata files from source dir'
});
parser.addCommand({
name: 'inspect', description: 'Inspect installer package metadata',
namedArgs: ['pkgName'],
minArgs: 1, maxArgs: 1,
callback: function() {
try {
const result = manager.inspectPackage(this.arguments.pkgName);
console.log(JSON.stringify(result, null, 2));
} catch (e) {
console.log(e.message);
that.trace(e.stack);
that._setFailedExitCode();
}
}
});
}
parse(args) {
this.parser.parse(args);
}
exec(options) {
options = _.defaults(options || {}, {args: null});
const args = options.args || process.argv.slice(2);
try {
this.parse(args);
} catch (e) {
if (e instanceof Parser.ParserExit) {
if (e.exitCode !== 0) this.error(e.message);
this.exitCode = e.exitCode;
return e.exitCode;
}
if (e instanceof Parser.ParserError) {
if (e.showHelp) {
this.parser.showHelp({throw: false});
}
this.exitCode = 1;
}
this.error(e.message);
this.trace(e.stack);
this._setFailedExitCode();
return this.exitCode;
}
const cmd = this.parser.selectedCommand;
if (!cmd) {
this.parser.showHelp({throw: false});
} else {
// Cmd was not already handled")
if (cmd.callback === null) {
this.trace(`Possibly unhandled command ${cmd.name}`);
}
}
return 0;
}
}
module.exports = NamiCliApp;