navy
Version:
Quick and powerful development environments using Docker and Docker Compose
581 lines (463 loc) • 14.6 kB
JavaScript
"use strict";
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.Navy = void 0;
exports.getLaunchedNavies = getLaunchedNavies;
exports.getLaunchedNavyNames = getLaunchedNavyNames;
exports.getNavy = getNavy;
var _invariant = _interopRequireDefault(require("invariant"));
var _eventemitter = require("eventemitter2");
var _promiseRetry = _interopRequireDefault(require("promise-retry"));
var _fs = _interopRequireDefault(require("../util/fs"));
var _driver = require("../driver");
var _configProvider = require("../config-provider");
var _util = require("./util");
var _state = require("./state");
var _config = require("../config");
var _errors = require("../errors");
var _pluginInterface = require("./plugin-interface");
var _middleware = require("./middleware");
var _httpProxy = require("../http-proxy");
var _externalIp = require("../util/external-ip");
var _serviceHost = require("../util/service-host");
/**
* A Navy instance
* @public
*/
class Navy extends _eventemitter.EventEmitter2 {
/**
* The name of the current Navy.
* @public
*/
/**
* The normalised name of the current Navy (name without whitespaces).
* @public
*/
constructor(name) {
super({
maxListeners: Number.MAX_SAFE_INTEGER,
wildcard: true,
delimiter: '.'
});
this.name = name;
this.normalisedName = (0, _util.normaliseNavyName)(name);
this._pluginsLoaded = false;
this._registeredCommands = {};
this._registeredMiddleware = [];
}
async ensurePluginsLoaded() {
if (this._pluginsLoaded) return;
const navyFile = await this.getNavyFile();
if (!navyFile) return;
await (0, _pluginInterface.loadPlugins)(this, navyFile);
this._pluginsLoaded = true;
}
/**
* Returns the current State for this Navy.
* @public
*/
async getState() {
return await (0, _state.getState)(this.normalisedName);
}
/**
* Returns an instance of the driver in use by this navy.
* Returns null if the navy hasn't been launched yet.
*/
async getDriver() {
const envState = await this.getState();
if (!envState || !envState.driver) {
return null;
}
const createDriver = (0, _driver.resolveDriverFromName)(envState.driver);
if (!createDriver) {
return null;
}
return createDriver(this);
}
/**
* Same as getDriver but will throw an error if the driver cannot be determined.
*/
async safeGetDriver() {
const driver = await this.getDriver();
if (!(await this.isInitialised())) {
throw new _errors.NavyNotInitialisedError(this.name);
}
(0, _invariant.default)(driver, "NO_DRIVER: Couldn't determine driver for navy %s", this.name);
return driver;
}
async getConfigProvider() {
const envState = await this.getState();
if (!envState || !envState.configProvider) {
return null;
}
const createConfigProvider = (0, _configProvider.resolveConfigProviderFromName)(envState.configProvider);
if (!createConfigProvider) {
return null;
}
return createConfigProvider(this);
}
async getNavyFile() {
if (!(await this.isInitialised())) {
throw new _errors.NavyNotInitialisedError(this.name);
}
const configProvider = await this.getConfigProvider();
(0, _invariant.default)(configProvider, "NO_CONFIG_PROVIDER: Couldn't determine config provider for navy %s", this.name);
const navyFilePath = await configProvider.getNavyFilePath();
try {
// $FlowIgnore: entry point to Navyfile.js has to be dynamic
return require(navyFilePath);
} catch (ex) {
return null;
}
}
/**
* Saves a new State for this Navy.
* @public
*/
async saveState(state) {
await (0, _state.saveState)(this.normalisedName, state);
await (0, _middleware.middlewareRunner)(this, state);
}
/**
* Registers a custom CLI command with the given name and callback function.
* Any command registered can be run using `navy run [name]`.
* @public
*/
registerCommand(name, action) {
this._registeredCommands[name] = action;
}
/**
* Registers a middleware reducer function. See [Writing Plugins](docs/writing-plugins.md).
* @public
*/
registerMiddleware(middlewareFn) {
this._registeredMiddleware.push(middlewareFn);
}
async invokeCommand(name, args) {
if (!this._registeredCommands[name]) {
throw new _errors.NavyError('Unknown command "' + name + '"');
}
await this._registeredCommands[name](args);
}
/**
* Returns whether this Navy is initialised and ready to launch services.
* @public
*/
async isInitialised() {
return (await this.getState()) != null;
}
/**
* Initialises this Navy with the given State.
* @public
*/
async initialise(opts) {
const state = {
services: {},
...opts,
driver: 'docker-compose'
};
await (0, _state.saveState)(this.normalisedName, state);
}
/**
* Deletes the state for this Navy. It won't be possible to interact with this Navy after calling this function
* unless you re-initialise it.
* @public
*/
async delete() {
await (0, _state.deleteState)(this.normalisedName);
}
/**
* Launches the given services for this Navy.
* @public
*/
async launch(serviceNames, opts) {
if (!serviceNames) serviceNames = await this.getLaunchedServiceNames();
const availableServices = await (await this.safeGetDriver()).getAvailableServiceNames();
const validServiceNames = serviceNames.filter(serviceName => {
if (availableServices.indexOf(serviceName) !== -1) {
return true;
}
console.log(`Unknown service name: ${serviceName}`);
return false;
});
const allServiceNamesInvalid = serviceNames.length > 0 && validServiceNames.length === 0;
if (allServiceNamesInvalid) {
return;
}
const state = await this.getState();
if (state) {
await (0, _middleware.middlewareRunner)(this, state);
}
await (await this.safeGetDriver()).launch(validServiceNames, opts);
await (0, _httpProxy.reconfigureHTTPProxy)();
}
/**
* @deprecated
*/
async relaunch(opts) {
return await this.reconfigure(opts);
}
/**
* Relaunches (reconfigures) all of the services running in this Navy.
* This will pick up any changes to the compose configuration.
* @public
*/
async reconfigure(opts) {
const services = await this.getLaunchedServiceNames();
if (services.length === 0) {
return;
}
await this.launch(undefined, opts);
}
/**
* Destroys all of the services in this Navy and deletes the state.
* @public
*/
async destroy() {
if (!(await this.isInitialised())) {
throw new _errors.NavyNotInitialisedError(this.name);
}
await (0, _httpProxy.reconfigureHTTPProxy)({
navies: (await getLaunchedNavyNames()).filter(navy => navy !== this.normalisedName)
});
try {
await (await this.safeGetDriver()).destroy();
} catch (ex) {}
await this.delete();
}
/**
* Returns a list of launched services for this Navy.
* @public
*/
async ps() {
return await (await this.safeGetDriver()).ps();
}
/**
* Starts the given services.
* @public
*/
async start(services) {
if (!services) services = await this.getLaunchedServiceNames();
await (0, _httpProxy.reconfigureHTTPProxy)();
await (await this.safeGetDriver()).start(services);
}
/**
* Stops the given services.
* @public
*/
async stop(services) {
if (!services) services = await this.getLaunchedServiceNames();
await (await this.safeGetDriver()).stop(services);
}
/**
* Restarts the given services.
* @public
*/
async restart(services) {
if (!services) services = await this.getLaunchedServiceNames();
await (await this.safeGetDriver()).restart(services);
}
/**
* Forcefully stops the given services.
* @public
*/
async kill(services) {
if (!services) services = await this.getLaunchedServiceNames();
await (await this.safeGetDriver()).kill(services);
}
/**
* Removes the given services. Requires the services to be stopped first.
* @public
*/
async rm(services) {
if (!services) services = await this.getLaunchedServiceNames();
await (await this.safeGetDriver()).rm(services);
}
/**
* Makes sure the images for the given services are up to date, and restarts any running
* services with new images.
* @public
*/
async update(services) {
// update all images by default
if (!services) services = await this.getAvailableServiceNames();
await (await this.safeGetDriver()).update(services);
}
async spawnLogStream(services) {
if (!services) services = await this.getLaunchedServiceNames();
await (await this.safeGetDriver()).spawnLogStream(services);
}
/**
* Locks down the given service to the given docker image tag.
* @public
*/
async useTag(service, tag) {
const state = (await this.getState()) || {};
await this.saveState({ ...state,
services: { ...state.services,
[service]: { ...(state.services || {})[service],
_tag: tag
}
}
});
await this.kill([service]);
await this.update([service]); // pull and launch
}
/**
* Resets the version of the given service if `useTag` was used.
* @public
*/
async resetTag(service) {
const state = (await this.getState()) || {};
await this.saveState({ ...state,
services: { ...state.services,
[service]: { ...(state.services || {})[service],
_tag: undefined
}
}
});
await this.kill([service]);
await this.launch([service], {
noDeps: true
});
}
async usePort(service, privatePort, externalPort) {
const state = (await this.getState()) || {};
await this.saveState({ ...state,
services: { ...state.services,
[service]: { ...(state.services || {})[service],
_ports: { ...((state.services || {})[service] || {})._ports,
[privatePort]: externalPort
}
}
}
});
await this.kill([service]);
await this.launch([service], {
noDeps: true
});
}
async resetPort(service, privatePort) {
const state = (await this.getState()) || {};
await this.saveState({ ...state,
services: { ...state.services,
[service]: { ...(state.services || {})[service],
_ports: { ...((state.services || {})[service] || {})._ports,
[privatePort]: undefined
}
}
}
});
await this.kill([service]);
await this.launch([service], {
noDeps: true
});
}
/**
* Waits for the given services to be healthy. Resolves when all services are healthy.
* @public
*/
async waitForHealthy(services, progressCallback, retryConfig = {
factor: 1.1,
retries: 30,
minTimeout: 200
}) {
if (services == null) {
services = (await this.ps()).filter(service => service && service.raw && service.raw.State.Health).map(service => service.name);
}
try {
await (0, _promiseRetry.default)(retryConfig, async retry => {
const ps = await this.ps();
const specifiedServices = ps.filter(service => services && services.indexOf(service.name) !== -1);
const servicesWithoutHealthInfo = specifiedServices.filter(service => !service.raw || !service.raw.State.Health).map(service => service.name);
if (servicesWithoutHealthInfo.length > 0) {
throw new _errors.NavyError('The specified services don\'t have health checks: ' + servicesWithoutHealthInfo.join(', '));
}
const serviceHealth = specifiedServices.map(service => ({
service: service.name,
health: service.raw && service.raw.State.Health.Status
}));
if (progressCallback) progressCallback(serviceHealth);
const unhealthy = serviceHealth.filter(service => service.health !== 'healthy');
if (unhealthy.length !== 0) {
retry('Timed out waiting for services to be healthy. Unhealthy services: ' + unhealthy.map(s => s.service).join(', '));
}
});
} catch (e) {
throw new _errors.NavyError(e.message);
}
return true;
}
/**
* Returns the external IP for accessing Docker and running services.
* @public
*/
async externalIP() {
const config = await (0, _config.getConfig)();
return await (0, _externalIp.getExternalIP)(config.externalIP);
}
/**
* Returns the external port for the given service and internal private port.
* @public
*/
async port(service, privatePort, index = 1) {
return await (await this.safeGetDriver()).port(service, privatePort, index);
}
/**
* Returns the URL for the given service which can be used to access it if it exposes
* a HTTP server.
* @public
*/
async url(service) {
const ps = await (await this.safeGetDriver()).ps(service);
return (0, _serviceHost.getUrlFromService)(ps.pop()) || (0, _serviceHost.createUrlForService)(service, this.normalisedName, await this.externalIP());
}
/**
* Returns an array of the names of the launched services for this Navy.
* @public
*/
async getLaunchedServiceNames() {
return await (await this.safeGetDriver()).getLaunchedServiceNames();
}
/**
* Returns an array of the names of all of the possible services for this Navy.
* @public
*/
async getAvailableServiceNames() {
return await (await this.safeGetDriver()).getAvailableServiceNames();
}
}
/**
* Returns a `Navy` instance from the given Navy name.
* @public
*/
exports.Navy = Navy;
function getNavy(navyName) {
(0, _invariant.default)(navyName, "NO_NAVY_PROVIDED: No Navy provided");
return new Navy(navyName);
}
/**
* Returns an array of `Navy` instances which are currently imported and launched.
* @public
*/
async function getLaunchedNavies() {
try {
const navyNames = await _fs.default.readdirAsync((0, _state.pathToNavys)());
return navyNames.filter(node => _fs.default.lstatSync((0, _state.pathToNavy)(node)).isDirectory()).map(name => getNavy(name));
} catch (ex) {
return [];
}
}
/**
* Returns an array of names of Navies which are currently imported and launched.
* @public
*/
async function getLaunchedNavyNames() {
try {
return await _fs.default.readdirAsync((0, _state.pathToNavys)());
} catch (ex) {
return [];
}
}