UNPKG

navy

Version:

Quick and powerful development environments using Docker and Docker Compose

581 lines (463 loc) 14.6 kB
"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 []; } }