| 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/node_modules/nami-core/lib/ |
Upload File : |
'use strict';
const Browser = require('./browser');
const _ = require('nami-utils/lodash-extra.js');
const nu = require('nami-utils');
const nfile = require('nami-utils/file');
const ncrypt = require('nami-utils/crypto');
const path = require('path');
const templates = require('nami-utils/templates');
const Logger = require('nami-logger');
const Registry = require('./registry.js');
const VmContext = require('./vm_context.js');
const repl = require('repl');
/**
* Allows packages to access already installed modules without having direct access to the manager
* @class
* @classdesc Handling an instance of the manager to packages would grant them too much power,
* by using this class, we only allow them to obtain references to already installed modules.
* @private
*/
class ModulesManager {
constructor(namiManager) {
this._manager = namiManager;
}
// We do not expose the full package, just a restricted minimum handler to it
_getModuleHandler(pkg) {
return pkg.getHandler();
}
getAllModules() {
const result = {};
const _this = this;
_.each(this._manager.listPackages(), function(pkg) {
result[pkg.id] = _this._getModuleHandler(pkg);
});
return result;
}
getRequiredModules(requiredModulesSpec) {
// requiredModulesSpec can be either:
// An array ['foo', 'bar']: Exposing later on 'foo' and 'bar' as $modules.foo and $modules.bar
// A hash {'foo': 'myFoo', bar: 'myBar'}: Exposing later on 'foo' and 'bar' as $modules.myFoo and $modules.myBar
// So we normalize here the spec to always be a hash (in the case of an array, a hash with keys===values)
requiredModulesSpec = requiredModulesSpec || {};
let requiredModules = null;
if (_.isArray(requiredModulesSpec)) {
requiredModules = {};
_.each(requiredModulesSpec, id => {
requiredModules[id] = id;
});
} else {
requiredModules = requiredModulesSpec;
}
const result = {};
const _this = this;
_.each(requiredModules, function(as, module) {
let pkg = null;
try {
pkg = _this._manager.search(module, {requestSingleResult: true});
} catch (e) {
throw new Error(`Error loading module requirements: ${e.message}`);
}
result[as] = _this._getModuleHandler(pkg);
});
return result;
}
}
/**
* Handles all the package-related operations
* @class
* @_private
*/
class Manager {
/**
* Constructs an instance of a Nami Manager
* @param {object} [options]
* @param {object} [options.logger=null] - External logger to use. If none is provided, a default one will be used
* @param {string} [options.logLevel=error] - When no external logger is provided, initial log level to use in the
* autogenerated logger
* @param {string} [options.installationPrefix=/opt/bitnami] - Default installation prefix. This can be overriden
* at a global level later oo or configured when installing packages
* @param {string} [options.registryPrefix=~/.nami] - Registry database location. This path will be used to store
* information about installed packages.
* @param {string} [options.encryptionPassword] - If an encryption password is provided, sensible information will be
* encrypted when using 'secure' serialization policy
* @param {string} [options.serializationPolicy=secure] - Configures how packages will serialize sensible information
* such as passwords. 'secure' will make them either
* to not be serialized or to be serialized encrypted, depending on the 'encryptionPassword' property. 'plain' will
* just write them in plain text, without any protection
* @constructs NamiManager
*/
constructor(options) {
options = _.defaults(options || {}, {
logger: null, logLevel: 'error',
installationPrefix: '/opt/bitnami',
encryptionPassword: null,
serializationPolicy: 'secure',
registryPrefix: '~/.nami'
});
this.exitCode = 0;
// Default root directory where all packages will be installed
this.installationPrefix = options.installationPrefix;
this.logger = options.logger || new Logger({level: options.logLevel, silenced: true});
// The registry takes care of managing the "database" of installed packages
this._registry = new Registry({
logger: this.logger, prefix: options.registryPrefix,
encryptionPassword: options.encryptionPassword,
serializationPolicy: options.serializationPolicy,
modulesManager: new ModulesManager(this)
});
// Allow asking the manager for the registryPrefix without going over the _registry
nu.delegate(this, {registryPrefix: 'prefix', reload: 'reload'}, this._registry);
}
/**
* Look for a package given a certain semver specification (sample, sample@3.5...)
* @param {string} searchTerm - Search specification, as a minimum, it should contain the id or name
* of the component to look for. It can also include contain a semversion specification
* (foo, sample@2.45, sample@ > 5.5 || < 4 ...).
* @param {object} [options]
* @param {boolean} [options.reload=false] - If enabled, do not use cached objects and peform a load from scratch
* @param {string|string[]} [options.searchBy=[id,name]] - Methods used for searching in order. The first one to
* succeed will skip the rest.
* @param {object} [options.requestSingleResult=false] - If enabled, fail if more than one component is found.
* It will also make the function return a single component instead of a list
* @throws Will throw an error if no components were found
* @throws Will throw an error if more than one components are found and 'requestSingleResult' was enabled
* @returns {object|object[]} Depending on if requestSingleResult was requested or not, the found Componet or list
* of Components
* @example
* // Looks for any version of a package
* manager.search('foo');
* @example
* // Looks for a package with an specific version
* manager.search('foo@6.7.3');
* @example
* // Looks for a package within a certain version range
* manager.search('foo@">3.4 && < 5.4"');
* @example
* // Looks for a package considering only IDs (ignoring the name)
* manager.search('foo', {searchBy: ['id']});
* // Returns a list: [{name: 'foo-name', version: '1.2', ...}]
* @example
* // Looks for a package considering only IDs (ignoring the name). One result:
* manager.search('foo', {searchBy: ['id'], requestSingleResult: true});
* // Returns a single element: {name: 'foo-name', version: '1.2', ...}
*/
search(searchTerm, options) {
options = _.sanitize(options, {searchBy: ['id', 'name'], requestSingleResult: false, reload: false});
const loadOptions = _.pick(options, ['reload']);
const packages = this._registry.search(searchTerm, _.pick(options, 'searchBy'));
const ids = _.pluck(packages, 'id');
if (_.isEmpty(packages)) {
throw new Error(`Cannot find any module matching the provided specification '${searchTerm}'`);
}
if (options.requestSingleResult) {
if (ids.length > 1) {
const printableResults = _.map(packages, data => `${data.name} ${data.version}`).join('\n');
throw new Error(`Found multiple occurrences for the specified term '${searchTerm}': \n${printableResults}`);
} else {
return this._registry.loadPackage(_.first(ids), loadOptions);
}
} else {
return _.map(ids, id => this._registry.loadPackage(id, loadOptions));
}
}
findByID(pkg, options) {
options = _.defaults(options || {}, {abortOnError: true});
const obj = this._registry.loadPackage(pkg);
if (obj === null && options.abortOnError) {
throw new Error(`Cannot find package ${pkg}`);
} else {
return obj;
}
}
// TODO: If we start allowing multiple versions of the same package we have to redefine how this is stored
getPackageId(obj) {
return obj.id;
}
_loadPackageFromDir(dir, options) {
return this._registry.loadPackageFromDir(dir, options);
}
// TODO: Perform real checks here
_validatePackageDirectory(pkgPath) {
if (_.some(['nami.json', 'bitnami.json'], f => nfile.exists(
path.join(pkgPath, f)
))) {
return;
} else {
throw new Error(`Cannot load package '${pkgPath}'`);
}
}
/**
* List installed packages
* @returns {object[]} A list of all installed packages
* @example
* // List all packages
* manager.listPackages();
*/
listPackages() {
return this._registry.loadAllPackages({softSchemaValidation: true, abortOnError: false});
}
// This is juat a quick way for testing it may not make sense in production in its current state
// Just documenting it as a reminder of its meaning until we improve/clean it
/**
* Refresh an already installed package metadata an JS scripts from a local package location. Useful for testing
* @private
* @param pkgLocation - Directory or zip file containing the package to refresh
* @example
* // Refresh already installed 'sample' updating its nami.json and JS scripts with those in path/to/sample
* manager.refreshMetadata('path/to/sample')
*/
refreshMetadata(pkg, options) {
this.logger.info(`Refreshing ${pkg} metadata`);
this._validatePackageDirectory(pkg);
options.srcDirRoot = pkg;
const obj = this._loadPackageFromDir(pkg, options);
const pkgId = this.getPackageId(obj);
const existingPackage = this.findByID(pkgId, {abortOnError: false});
if (existingPackage === null) {
throw new Error(`${pkgId} is not installed. It is not possible to update its metadata`);
}
this._registry.updateResources(existingPackage, obj);
}
/**
* Inspect installer package metadata
* @param pkg - Package reference. It can be its id or name, including an optional semver
*/
inspectPackage(pkg) {
return this.search(pkg, {requestSingleResult: true}).serializeData();
}
_populateObjectFromCmdFlags(obj, options) {
options = _.sanitize(options, {validateRequiredAndNotProvidedOptions: true, args: [], hashArgs: {}});
if (!_.isEmpty(options.hashArgs)) {
// We will also validate 'args', which will validate required options so don't fail here just yet
obj.parser.parseData(options.hashArgs, {force: true});
}
obj.parser.parse(options.args || [], {
abortIfRequiredAndNotProvided: options.validateRequiredAndNotProvidedOptions
});
obj.validate({validateRequiredAndNotProvidedOptions: options.validateRequiredAndNotProvidedOptions});
}
/**
* Initialize an already 'unpacked' package. It will look for a package in the 'unpacked' statate and
* execute their postInstall
* @param package - Package reference. It can be its id or name, including an optional semver.
* @param {object} [options]
* @param {boolean} [options.force=false] - If enabled, initialize the package even if fully installed
* @param {string[]} [options.args=[]] - Extra arguments to pass on in raw format (as got from process.argv)
* @param {object} [options.hashArgs=[]] - Extra arguments to pass on in hash format
*/
initializePackage(pkg, options) {
options = _.opts(options, {force: false, args: [], hashArgs: {}});
const obj = this.search(pkg, {requestSingleResult: true});
switch (obj.lifecycle) {
case 'unpacked':
this.logger.info(`Initializing ${pkg}`);
break;
case 'installed':
if (options.force) {
this.logger.warn(`Forcing initialization of package ${pkg}`);
} else {
throw new Error(`Package ${pkg} seems to be already fully installed`);
}
break;
default:
throw new Error(`Package cannot be initialized (incompatible lifecycle ${obj.lifecycle})`);
}
this._populateObjectFromCmdFlags(obj, _.pick(options, ['args', 'hashArgs']));
try {
obj.install({onlyPostInstall: true});
this.logger.info(`${obj.name} successfully initialized`);
} catch (e) {
this.exitCode = 1;
throw e;
} finally {
this._registry.update(obj);
}
return obj;
}
// Internal method that allows extra installation options: skipPostInstall and onlyPostInstall,
// which are selected based on higher level commands
_install(pkgLocation, options) {
options = _.opts(options, {
installPrefix: this.installationPrefix,
skipPostInstall: false, onlyPostInstall: false,
force: false, args: [], hashArgs: {}
});
pkgLocation = nfile.normalize(pkgLocation);
if (!nfile.exists(pkgLocation)) throw new Error(`'${pkgLocation}' does not exists`);
this.logger.info(`${options.skipPostInstall ? 'Unpacking' : 'Installing'} ${pkgLocation}`);
this._validatePackageDirectory(pkgLocation);
options.srcDirRoot = pkgLocation;
options.initializing = true;
const obj = this._loadPackageFromDir(pkgLocation, options);
const pkgId = this.getPackageId(obj);
const existingPackage = this._registry.getPkgData(pkgId, {abortOnError: false});
if (existingPackage !== null) {
if (options.force) {
this.logger.warn(`Reinstalling package ${pkgId}`);
} else {
const isFullyInstalled = existingPackage.lifecycle === 'installed';
throw new Error(`Package ${pkgId} seems to be ${isFullyInstalled ? 'already' : 'partially'} installed`);
}
}
// Parse command line arguments
// If we are not doing the post-install, don't throw errors
// if required options are not provided
const validateRequiredAndNotProvided = !options.skipPostInstall;
this._populateObjectFromCmdFlags(
obj,
_.extend(_.pick(options, ['args', 'hashArgs']), {
validateRequiredAndNotProvidedOptions: validateRequiredAndNotProvided
})
);
if (!nfile.writable(obj.installdir)) {
throw new Error(`The selected installdir '${obj.installdir}' is not writable by the current user`);
}
try {
obj.install(options);
this.logger.info(
`${obj.name} successfully ${options.skipPostInstall ? 'unpacked' : 'installed'} into ${obj.installdir}`
);
} catch (e) {
this.exitCode = 1;
throw e;
} finally {
if (this._shouldRegisterComponent(obj)) {
this._registry.register(obj);
}
}
return obj;
}
_shouldRegisterComponent(obj) {
// If lifecycle is null, it means it did not even reached the first
// millestone (preInstallChecks) so we do not want to register it
return obj.lifecycle !== null;
}
/**
* Unpacks a package. It will basically perform the first steps of the installation, skipping from the
* postInstallation onwards. The package will be registered with 'unpacked' status, and will need to be
* initialized to be usable
* @param pkgLocation - Directory or zip file containing the package to install
* @param {object} [options]
* @param {string} [options.installPrefix] - If provided, it will override the global installation prefix
* @param {boolean} [options.force=false] - Force re-unpacking of already unpacked packages
* @param {string[]} [options.args=[]] - Extra arguments to pass on in raw format (as got from process.argv)
* @param {object} [options.hashArgs=[]] - Extra arguments to pass on in hash format
*/
unpack(pkgLocation, options) {
options = _.sanitize(options, {installPrefix: this.installationPrefix, args: [], hashArgs: {}, force: false});
return this._install(pkgLocation, _.extend(options, {skipPostInstall: true}));
}
/**
* Installs a package
* @param pkgLocation - Directory or zip file containing the package to install
* @param {object} [options]
* @param {string} [options.installPrefix] - If provided, it will override the global installation prefix
* @param {boolean} [options.force=false] - Force reinstallation of packages
* @param {string[]} [options.args=[]] - Extra arguments to pass on in raw format (as got from process.argv)
* @example
* // Installs package providing arguments directly from process.argv
* manger.install('/path/to/package', {args: process.argv.slice(1)})
* // Installs package providing password in raw format
* manger.install('/path/to/package', {args: ['--password=foo']})
*/
install(pkgLocation, options) {
options = _.sanitize(options, {installPrefix: this.installationPrefix, force: false, args: [], hashArgs: {}});
return this._install(pkgLocation, options);
}
/**
* Tests a previously installed package
* @param pkg - Package reference. It can be its id or name, including an optional semver
* @param callback - Function to execute after the tests finish
* @param {object} [options]
* @param {string} [options.testDir] - Directory containing the test files to execute.
* Defaults to the package 'test' directory
* @param {string[]} [options.exclude=[]] - List of patterns used to exclude test files from execution
* @param {string[]} [options.include=['*']] - List of patterns used to include test files in the execution
* @param {regexp} [options.grep=null] - If provided, only execute tests maching this pattern
*/
test(pkg, callback, options) {
const obj = this.search(pkg, {requestSingleResult: true});
this.logger.info(`Testing ${pkg}`);
try {
obj.runTests(callback, options);
} catch (e) {
this.exitCode = 1;
throw e;
}
}
/**
* Uninstalls a previously installed package
* @param pkg - Package reference. It can be its id or name, including an optional semver
* @throws Will throw an error trying to uninstall a package as a regular user previously installed as root
*/
uninstall(pkg) {
const obj = this.search(pkg, {requestSingleResult: true});
if (obj.installedAsRoot && !nu.os.runningAsRoot()) {
throw new Error(`This package was installed as root. Refusing to uninstall without admin privileges`);
}
this.logger.info(`Uninstalling ${pkg}`);
try {
obj.uninstall();
} catch (e) {
this.exitCode = 1;
throw e;
} finally {
if (obj.lifecycle === 'uninstalled') {
this._registry.unregister(obj);
}
}
}
_getVmContextData() {
return {
require: require,
$modules: new ModulesManager(this).getAllModules(), $hb: templates, $manager: this,
$Browser: Browser,
$util: nu.util, $file: nu.file, $os: nu.os, $build: nu.build,
$net: nu.net,
$crypt: ncrypt
};
}
/**
* Evals filename with all the nami packages populated
* @returns {*} The result of the last line executed
* @example
* // file.js contains 7 + 4
* manager.evalFile('file.js');
* // Returns 11
*/
evalFile(script, options) {
options = _.opts(options, {package: null});
if (options.package) {
const obj = this.search(options.package, {requestSingleResult: true});
return obj.evalFile(script);
} else {
const context = new VmContext(this._getVmContextData());
return context.evalFile(script, {displayErrors: false});
}
}
/**
* Opens an intactive console with a global or a given package environment
* @param [package] - If provided, the opened console will also include a reference to the package as $app.
*/
console(pkg) {
if (pkg) {
const obj = this.search(pkg, {requestSingleResult: true});
obj.console({globals: {$manager: this}});
} else {
const contextData = this._getVmContextData();
const context = repl.start('nami> ').context;
_.extend(context, contextData);
}
}
}
module.exports = Manager;