UNPKG

@flowfuse/flowfuse

Version:

An open source low-code development platform

434 lines (418 loc) • 14.7 kB
/** * Stub Container driver * * Handles the creation and delegation of containers to back Projects * * This Stub driver doesn't start any real containers, just keeps state in memory * * @module stub * @memberof forge.containers.drivers * */ const { normalize } = require('path') const nrUtil = require('@node-red/util') // eslint-disable-line const forgeUtils = require('../../db/utils') const list = {} const files = {} module.exports = { START_DELAY: 500, STOP_DELAY: 250, /** * Initialises this driver * * Use app.db.models.Project.findAll() to get a list of all projects * and do the work to synchronise the internal state with that list * * @param {string} app - the forge application * @param {object} options - A set of configuration options for the driver * @return {forge.containers.ProjectArguments} */ init: async (app, options) => { this._options = options this._app = app const projects = await this._app.db.models.Project.findAll() projects.forEach(project => { if (project.state !== 'suspended') { const p = { id: project.id, state: 'running', url: project.url, options: {}, meta: { foo: 'bar' } } list[project.id] = p } }) return { stack: { properties: { nodered: { label: 'Node-RED Version', validate: '^(0|[1-9]\\d*)(\\.(0|[1-9]\\d*|x|\\*)(\\.(0|[1-9]\\d*|x|\\*))?)?$', invalidMessage: 'Invalid version number - expected x.y.z' } } } } }, /** * Start a new Project * * If the driver has any driver-specific settings for the project, then * it can use the following to store them in the app database: * * Single property * await project.updateSetting("pid",123) * * Bulk update * await project.updateSettings({ * pid: pid, * path: directory, * port: port, * }) * * This *must* generate a clean set of auth tokens to pass to the launcher. * Calling this function will replace any existing tokens with a new set - * there can only be one active launcher per project. * * const authTokens = await project.refreshAuthTokens(); * * Once created, this *must* set the `url` property of the project: * * project.url = "http://localhost:" + port; * await project.save() * * * @param {Project} project - the project model instance * @return {forge.containers.Project} */ start: async (project) => { this._app.log.info(`[stub driver] Starting ${project.id}`) if (!list[project.id] || list[project.id].state === 'suspended') { list[project.id] = { id: project.id, state: 'starting', url: `http://${project.name}.${this._options.domain}`, meta: { foo: 'bar' }, stack: project.ProjectStack.hashid } project.url = list[project.id].url await project.save() if (await project.getSetting('stubProjectToken') === undefined) { const stubProjectToken = forgeUtils.generateToken(8) await project.updateSetting('stubProjectToken', stubProjectToken) } if (project.name === 'stub-fail-start') { return new Promise((resolve, reject) => { setTimeout(() => { delete list[project.id] reject(new Error('failing to start project')) }, 500) }) } else { const startTime = project.name === 'stub-slow-start' ? 6000 : module.exports.START_DELAY return new Promise((resolve, reject) => { setTimeout(() => { list[project.id].state = 'running' resolve() }, startTime) }) } } else { throw new Error('Name already exists') } }, /** * Stop a project * @param {Project} project - the project model instance * @return {Object} */ stop: async (project) => { this._app.log.info(`[stub driver] Stopping ${project.id}`) if (list[project.id]) { return new Promise((resolve, reject) => { setTimeout(() => { list[project.id].state = 'suspended' resolve() }, module.exports.STOP_DELAY) }) } else { throw new Error(`${project.id} not found`) } }, /** * Removes a Project * @param {Project} project - the project model instance * @return {Object} */ remove: async (project) => { this._app.log.info(`[stub driver] Removing ${project.id}`) if (list[project.id]) { return new Promise((resolve, reject) => { setTimeout(() => { delete list[project.id] resolve() }, 250) }) } }, /** * Retrieves details of a project's container * @param {Project} project - the project model instance * @return {Object} */ details: async (project) => { return list[project.id] }, /** * Returns an object holding the values to plug into a Projects `settings.js` * file by the nr-launcher * * @param {Project} project - the project model instance * @return {Object} */ settings: async (project) => { const settings = { stubProjectToken: await project.getSetting('stubProjectToken'), isStubDriver: true } return settings }, /** * Starts a Project's flows * @param {Project} project - the project model instance * @param {} options * @return {forge.Status} */ startFlows: async (project, options) => { if (list[project.id]) { this._app.log.info(`[stub driver] Start flows ${project.id}`) list[project.id].state = 'running' } }, /** * Stops a Project's flows * @param {Project} project - the project model instance * @param {} options * @return {forge.Status} */ stopFlows: async (project, options) => { if (list[project.id]) { this._app.log.info(`[stub driver] Stop flows ${project.id}`) list[project.id].state = 'stopped' } }, /** * Restarts a Project's flows * @param {Project} project - the project model instance * @param {} options * @return {forge.Status} */ restartFlows: async (project, options) => { this._app.log.info(`[stub driver] Restarting flows ${project.id}`) }, /** * Get a Project's logs * @param {Project} project - the project model instance * @return {array} logs */ logs: async (project) => { const oneHour = 360000 return [ { level: 'system', msg: 'Fake Log Entry', ts: `${Date.now() - oneHour}`, src: 'one' }, { level: 'system', msg: 'Starting Node-RED', ts: `${Date.now() - oneHour / 2}`, src: 'one' }, { level: 'info', msg: '\n\nMulti Line Message\n===================\n', ts: `${Date.now() - oneHour / 4}`, src: 'one' }, { level: 'warn', msg: 'This is the voice of the Mysterons. We know that you can hear us Earthmen.', ts: `${Date.now() - oneHour / 5}`, src: 'one' }, { level: 'error', msg: 'Captain Scarlet is indestructible', ts: `${Date.now()}`, src: 'two' } ] }, /** * Shutdown Driver */ shutdown: async () => { }, getDefaultStackProperties: () => { return { nodered: '3.0.2', ...this._app.config.driver.options?.default_stack } }, revokeUserToken: async (project, token) => { }, // File API // Static Assets API listFiles: async (instance, filePath) => { if (!list[instance.id] || list[instance.id].state === 'suspended') { throw new Error('Cannot access instance files') } if (!files[instance.id]) { files[instance.id] = {} } const pathDots = filePath.replace('/', '.') const response = { meta: {}, files: [], count: 0 } try { const dir = pathDots ? nrUtil.util.getObjectProperty(files[instance.id], pathDots) : files[instance.id] Object.keys(dir).forEach(entry => { if (typeof dir[entry] === 'object') { response.files.push({ name: entry, type: 'directory', lastModified: new Date().toISOString() }) } else { response.files.push({ name: entry, type: 'file', size: dir[entry].length, lastModified: new Date().toISOString() }) } response.count++ }) return response } catch (err) { if (err.message === 'Cannot convert undefined or null to object' || err.message.startsWith('Cannot read properties of undefined')) { const newErr = new Error('not found') newErr.statusCode = 404 throw newErr } else { throw err } } }, updateFile: async (instance, filePath, update) => { if (!list[instance.id] || list[instance.id].state === 'suspended') { throw new Error('Cannot access instance files') } if (!files[instance.id]) { files[instance.id] = {} } // const pathDots = filePath.replace('/','.') // const dir = pathDots ? nrUtil.util.getObjectProperty(files[instance.id], pathDots) : files[instance.id] }, deleteFile: async (instance, filePath) => { if (!list[instance.id] || list[instance.id].state === 'suspended') { throw new Error('Cannot access instance files') } if (!files[instance.id]) { files[instance.id] = {} } const parts = normalize(filePath).split('/') const filename = parts.pop() if (parts.indexOf('..') !== -1) { if (parts.indexOf('..') === 0) { const newErr = new Error('not found') newErr.statusCode = 404 throw newErr } else { while (parts.indexOf('..') !== -1) { parts.splice(parts.indexOf('..') - 1, 2) } } } const pathDots = parts.join('.') try { const dir = pathDots ? nrUtil.util.getObjectProperty(files[instance.id], pathDots) : files[instance.id] delete dir[filename] } catch (err) { if (err.message === 'Cannot convert undefined or null to object' || err.message.startsWith('Cannot read properties of undefined')) { const newErr = new Error('not found') newErr.statusCode = 404 throw newErr } else { throw err } } }, createDirectory: async (instance, filePath, directoryName) => { if (!list[instance.id] || list[instance.id].state === 'suspended') { throw new Error('Cannot access instance files') } if (!files[instance.id]) { files[instance.id] = {} } const pathDots = filePath.replace('/', '.') const nameDots = directoryName.replace('/', '.') try { const dir = pathDots ? nrUtil.util.getObjectProperty(files[instance.id], pathDots) : files[instance.id] nrUtil.util.setObjectProperty(dir, nameDots, {}, true) } catch (err) { if (err.message === 'Cannot convert undefined or null to object' || err.message.startsWith('Cannot read properties of undefined')) { const newErr = new Error('not found') newErr.statusCode = 404 throw newErr } else { throw err } } }, uploadFile: async (instance, filePath, readableStream) => { if (!list[instance.id] || list[instance.id].state === 'suspended') { throw new Error('Cannot access instance files') } if (!files[instance.id]) { files[instance.id] = {} } const parts = normalize(filePath).split('/') const filename = parts.pop() if (parts.indexOf('..') !== -1) { if (parts.indexOf('..') === 0) { const newErr = new Error('not found') newErr.statusCode = 404 throw newErr } else { while (parts.indexOf('..') !== -1) { parts.splice(parts.indexOf('..') - 1, 2) } } } const pathDots = parts.join('.') try { const dir = pathDots ? nrUtil.util.getObjectProperty(files[instance.id], pathDots) : files[instance.id] dir[filename] = readableStream.toString('utf-8') } catch (err) { if (err.message === 'Cannot convert undefined or null to object' || err.message.startsWith('Cannot read properties of undefined')) { const newErr = new Error('not found') newErr.statusCode = 404 throw newErr } else { throw err } } }, // 3rd party broker startBrokerAgent: async (broker) => {}, stopBrokerAgent: async (broker) => {}, getBrokerAgentState: async (broker) => { return { connected: true, error: '' } }, sendBrokerAgentCommand: async (broker, command) => {} }