From 86545ac767bd2e554ed4dc288d4d46bd12ff8ccd Mon Sep 17 00:00:00 2001 From: Guillaume Date: Fri, 25 Feb 2022 14:11:28 +0100 Subject: [PATCH] Enable loading of multiple mocks Make running multiple mocks at once easier by allowing user to provide multiple paths to the `--data` flag. This is compatible with both managed processes and foreground processes. - Remove deprecated flags from legacy export: `--all`, `--index`, `--name` - Legacy export run all the mocks by default - make `--data`, `--pname`, `--port` and `--hostname` flags multiple, in order to provide list of mocks - Dockerize command now supports copying multiple mocks and exposing multiple ports - Update server logs to show the mock name to be able to filter logs by mock Closes #57 --- README.md | 33 ++--- src/commands/dockerize.ts | 76 ++++++----- src/commands/start.ts | 176 ++++++++++-------------- src/constants/command.constants.ts | 27 +--- src/constants/docker.constants.ts | 8 +- src/constants/messages.constants.ts | 5 +- src/libs/data.ts | 184 +++++++++++--------------- src/libs/server.ts | 19 ++- src/libs/utils.ts | 39 ------ test/specs/dockerize-command.spec.ts | 154 +++++++++++++++------ test/specs/legacy-export-file.spec.ts | 45 +------ test/specs/run-multiple-mocks.spec.ts | 31 +++-- 12 files changed, 363 insertions(+), 434 deletions(-) diff --git a/README.md b/README.md index 29fdcac..4fa299f 100644 --- a/README.md +++ b/README.md @@ -79,7 +79,7 @@ $ mockoon-cli start --data https://domain.com/your-environment-file.json > **Use a legacy export file** > -> While we recommend using the method above to launch your mocks with the CLI, you can still use Mockoon's [legacy export files](https://mockoon.com/docs/latest/mockoon-data-files/import-export-mockoon-format/) and the dedicated flags `--index`, `--name` or `--all`. +> While we recommend using the method above to launch your mocks with the CLI, you can still use Mockoon's [legacy export files](https://mockoon.com/docs/latest/mockoon-data-files/import-export-mockoon-format/). ### Use an OpenAPI specification file @@ -111,9 +111,10 @@ Mockoon's CLI has been tested on Node.js versions 12, 14, 15 and 16. ### `mockoon-cli start` -Starts a mock API from a Mockoon's environment file. +Starts one (or more) mock API from Mockoon's environment file(s). The process will be created by default with the name and port of the Mockoon's environment. You can override these values by using the `--port` and `--pname` flags. +`--data`, `--port`, `--pname` and `--hostname` flags support multiple entries to run multiple mock APIs at once (see examples below). Using the `--daemon-off` flag will keep the CLI in the foreground. The mock API process will not be [managed by PM2](#pm2). When running as a blocking process, all the logs are sent to both stdout (console) and the [usual files](logs). @@ -124,27 +125,22 @@ USAGE $ mockoon-cli start OPTIONS - -d, --data=data [required] Path or URL to your Mockoon file - -N, --pname=pname Override process name - -p, --port=port Override environment's port - -l, --hostname=0.0.0.0 Override default listening hostname (0.0.0.0) + -d, --data [required] Path(s) or URL(s) to your Mockoon file(s) + -N, --pname Override process(es) name(s) + -p, --port Override environment(s) port(s) + -l, --hostname=0.0.0.0 Override default listening hostname(s) (0.0.0.0) -t, --log-transaction Log the full HTTP transaction (request and response) -r, --repair If the data file seems too old, or an invalid Mockoon file, migrate/repair without prompting -D, --daemon-off Keep the CLI in the foreground and do not manage the process with PM2 - -a, --all [deprecated] Run all environments in the legacy export file - -i, --index=index [deprecated] Select by environment's index in the legacy export file - -n, --name=name [deprecated] Seelct by environment name in the legacy export file -h, --help Show CLI help EXAMPLES $ mockoon-cli start --data ~/data.json + $ mockoon-cli start --data ~/data1.json ~/data2.json --port 3000 3001 --pname mock1 mock2 $ mockoon-cli start --data https://file-server/data.json $ mockoon-cli start --data ~/data.json --pname "proc1" $ mockoon-cli start --data ~/data.json --daemon-off $ mockoon-cli start --data ~/data.json --log-transaction - $ mockoon-cli start --data ~/data.json --all - $ mockoon-cli start --data ~/data.json --name "Mock environment" - $ mockoon-cli start --data ~/data.json --index 0 ``` ### `mockoon-cli list [ID]` @@ -194,10 +190,10 @@ EXAMPLE ### `mockoon-cli dockerize` -Generates a Dockerfile used to build a self-contained image of a mock API. After building the image, no additional parameters will be needed when running the container. +Generates a Dockerfile used to build a self-contained image of one or more mock API. After building the image, no additional parameters will be needed when running the container. This command takes similar flags as the [`start` command](#mockoon-start). -Please note that this command will extract your Mockoon environment from the file you provide and put it side by side with the generated Dockerfile. Both files are required in order to build the image. +Please note that this command will copy your Mockoon environment from the file you provide and put it side by side with the generated Dockerfile. Both files are required in order to build the image. For more information on how to build the image: [Using the dockerize command](#using-the-dockerize-command) @@ -206,17 +202,16 @@ USAGE $ mockoon-cli dockerize OPTIONS - -d, --data=data [required] Path or URL to your Mockoon file - -p, --port=port Override environment's port + -d, --data [required] Path or URL to your Mockoon file + -p, --port Override environment's port -o, --output [required] Generated Dockerfile path and name (e.g. `./Dockerfile`) -t, --log-transaction Log the full HTTP transaction (request and response) -r, --repair If the data file seems too old, or an invalid Mockoon file, migrate/repair without prompting - -i, --index=index [deprecated] Select by environment's index in the legacy export file - -n, --name=name [deprecated] Select by environment name in the legacy export file -h, --help Show CLI help EXAMPLES $ mockoon-cli dockerize --data ~/data.json --output ./Dockerfile + $ mockoon-cli dockerize --data ~/data1.json ~/data2.json --output ./Dockerfile $ mockoon-cli dockerize --data https://file-server/data.json --output ./Dockerfile ``` @@ -327,7 +322,7 @@ The `transaction` model can be found [here](https://github.com/mockoon/commons/b ## PM2 -Mockoon CLI uses [PM2](https://pm2.keymetrics.io/) to start, stop or list the running mock APIs. Therefore, you can directly use PM2 commands to manage the processes. +Mockoon CLI uses [PM2](https://pm2.keymetrics.io/) to start, stop or list the running mock APIs when you are not using the `--daemon-off` flag. Therefore, you can directly use PM2 commands to manage the processes. ## Mockoon's documentation diff --git a/src/commands/dockerize.ts b/src/commands/dockerize.ts index 5276798..16ebd02 100644 --- a/src/commands/dockerize.ts +++ b/src/commands/dockerize.ts @@ -8,15 +8,16 @@ import { Config } from '../config'; import { commonFlags, startFlags } from '../constants/command.constants'; import { DOCKER_TEMPLATE } from '../constants/docker.constants'; import { Messages } from '../constants/messages.constants'; -import { parseDataFile, prepareData } from '../libs/data'; -import { portIsValid, promptEnvironmentChoice } from '../libs/utils'; +import { parseDataFiles, prepareEnvironment } from '../libs/data'; +import { portIsValid } from '../libs/utils'; export default class Dockerize extends Command { public static description = - 'Create a Dockerfile to build a self-contained image of a mock API'; + 'Create a Dockerfile to build a self-contained image of one or more mock API'; public static examples = [ '$ mockoon-cli dockerize --data ~/data.json --output ./Dockerfile', + '$ mockoon-cli dockerize --data ~/data1.json ~/data2.json --output ./Dockerfile', '$ mockoon-cli dockerize --data https://file-server/data.json --output ./Dockerfile' ]; @@ -31,38 +32,44 @@ export default class Dockerize extends Command { }; public async run(): Promise { - let { flags: userFlags } = this.parse(Dockerize); + const { flags: userFlags } = this.parse(Dockerize); const resolvedDockerfilePath = pathResolve(userFlags.output); const dockerfilePath: ParsedPath = pathParse(resolvedDockerfilePath); - let environmentInfo: { name: any; port: any; dataFile: string }; + const parsedEnvironments = await parseDataFiles(userFlags.data); + userFlags.data = parsedEnvironments.filePaths; - const environments = await parseDataFile(userFlags.data); - - userFlags = await promptEnvironmentChoice(userFlags, environments); + const environmentsInfo: { name: any; port: any; dataFile: string }[] = []; try { - environmentInfo = await prepareData({ - environments, - options: { - index: userFlags.index, - name: userFlags.name, - port: userFlags.port - }, - dockerfileDir: dockerfilePath.dir, - repair: userFlags.repair - }); - } catch (error: any) { - this.error(error.message); - } - if (!portIsValid(environmentInfo.port)) { - this.error(format(Messages.CLI.PORT_IS_NOT_VALID, environmentInfo.port)); - } + for ( + let envIndex = 0; + envIndex < parsedEnvironments.environments.length; + envIndex++ + ) { + const environmentInfo = await prepareEnvironment({ + environment: parsedEnvironments.environments[envIndex], + userOptions: { + port: userFlags.port[envIndex] + }, + dockerfileDir: dockerfilePath.dir, + repair: userFlags.repair + }); + + environmentsInfo.push(environmentInfo); + + if (!portIsValid(environmentInfo.port)) { + this.error( + format(Messages.CLI.PORT_IS_NOT_VALID, environmentInfo.port) + ); + } + } - try { const dockerFile = mustacheRender(DOCKER_TEMPLATE, { - port: environmentInfo.port, - filePath: pathParse(environmentInfo.dataFile).base, + ports: environmentsInfo.map((environmentInfo) => environmentInfo.port), + filePaths: environmentsInfo.map( + (environmentInfo) => pathParse(environmentInfo.dataFile).base + ), version: Config.version, // passing more args to the dockerfile template, only make sense for log transaction yet as other flags are immediately used during the file creation and data preparation args: userFlags['log-transaction'] ? ', "--log-transaction"' : '' @@ -76,10 +83,17 @@ export default class Dockerize extends Command { this.log( Messages.CLI.DOCKERIZE_BUILD_COMMAND, dockerfilePath.dir, - environmentInfo.name, - environmentInfo.port, - environmentInfo.port, - environmentInfo.name + environmentsInfo.length > 1 + ? 'mockoon-mocks' + : environmentsInfo[0].name, + environmentsInfo.reduce( + (portsString, environmentInfo) => + `${portsString ? portsString + ' ' : portsString}-p ${ + environmentInfo.port + }:${environmentInfo.port}`, + '' + ), + environmentsInfo.length > 1 ? 'mockoon-mocks' : environmentsInfo[0].name ); } catch (error: any) { this.error(error.message); diff --git a/src/commands/start.ts b/src/commands/start.ts index b1b06c1..2216925 100644 --- a/src/commands/start.ts +++ b/src/commands/start.ts @@ -7,15 +7,10 @@ import { format } from 'util'; import { Config } from '../config'; import { commonFlags, startFlags } from '../constants/command.constants'; import { Messages } from '../constants/messages.constants'; -import { parseDataFile, prepareData } from '../libs/data'; +import { parseDataFiles, prepareEnvironment } from '../libs/data'; import { ProcessListManager, ProcessManager } from '../libs/process-manager'; import { createServer } from '../libs/server'; -import { - getDirname, - portInUse, - portIsValid, - promptEnvironmentChoice -} from '../libs/utils'; +import { getDirname, portInUse, portIsValid } from '../libs/utils'; interface EnvironmentInfo { name: string; @@ -28,11 +23,24 @@ interface EnvironmentInfo { logTransaction?: boolean; } +type StartFlags = { + pname: string[]; + hostname: string[]; + 'daemon-off': boolean; + container: boolean; + data: string[]; + port: number[]; + 'log-transaction': boolean; + repair: boolean; + help: void; +}; + export default class Start extends Command { - public static description = 'Start a mock API'; + public static description = 'Start one or more mock API'; public static examples = [ '$ mockoon-cli start --data ~/data.json', + '$ mockoon-cli start --data ~/data1.json ~/data2.json --port 3000 3001 --pname mock1 mock2', '$ mockoon-cli start --data https://file-server/data.json', '$ mockoon-cli start --data ~/data.json --pname "proc1"', '$ mockoon-cli start --data ~/data.json --daemon-off', @@ -44,18 +52,21 @@ export default class Start extends Command { ...startFlags, pname: flags.string({ char: 'N', - description: 'Override the process name' + description: 'Override the process(es) name(s)', + multiple: true, + default: [] }), hostname: flags.string({ char: 'l', - description: 'Listening hostname/address' + description: 'Listening hostname(s)', + multiple: true, + default: [] }), 'daemon-off': flags.boolean({ char: 'D', description: 'Keep the CLI in the foreground and do not manage the process with PM2', - default: false, - exclusive: ['all'] + default: false }), /** * /!\ Undocumented flag. @@ -78,14 +89,33 @@ export default class Start extends Command { public async run(): Promise { const { flags: userFlags } = this.parse(Start); + let environmentsInfo: EnvironmentInfo[] = []; + try { - const environmentInfoList = await this.getEnvironmentInfoList(userFlags); + // We are in a container, env file is ready and relative to the Dockerfile + if (userFlags.container) { + environmentsInfo = await this.getEnvInfoListFromContainerFlag( + userFlags + ); + } else { + const parsedEnvironments = await parseDataFiles(userFlags.data); + userFlags.data = parsedEnvironments.filePaths; + + environmentsInfo = await this.getEnvironmentsInfo( + userFlags, + parsedEnvironments.environments + ); + } - for (const environmentInfo of environmentInfoList) { + for (const environmentInfo of environmentsInfo) { await this.validatePort(environmentInfo.port, environmentInfo.hostname); await this.validateName(environmentInfo.name); - await this.runEnvironment(environmentInfo, userFlags['daemon-off']); + if (userFlags['daemon-off']) { + this.startForegroundProcess(environmentInfo); + } else { + await this.startManagedProcess(environmentInfo); + } } } catch (error: any) { this.error(error.message); @@ -94,17 +124,6 @@ export default class Start extends Command { } } - private async runEnvironment( - environmentInfo: EnvironmentInfo, - daemonOff = false - ) { - if (daemonOff) { - this.startForegroundProcess(environmentInfo); - } else { - await this.startManagedProcess(environmentInfo); - } - } - private async addProcessToProcessListManager( environmentInfo: EnvironmentInfo, process: Proc @@ -202,76 +221,56 @@ export default class Start extends Command { this.error(format(Messages.CLI.PROCESS_START_LOG_ERROR, name, name)); } - private async getEnvironmentInfoList(userFlags): Promise { - // We are in a container, env file is ready and relative to the Dockerfile - if (userFlags.container) { - return this.getEnvInfoListFromContainerFlag(userFlags); - } - - return this.getEnvInfoListFromNonContainerFlag(userFlags); - } - private async getEnvInfoListFromContainerFlag( - userFlags + userFlags: StartFlags ): Promise { - const environment: Environment = await readJSONFile( - userFlags.data, - 'utf-8' - ); - let protocol = 'http'; + const environmentsInfo: EnvironmentInfo[] = []; - if (environment.tlsOptions.enabled) { - protocol = 'https'; - } + for (const dataFile of userFlags.data) { + const environment: Environment = await readJSONFile(dataFile, 'utf-8'); + + let protocol = 'http'; - return [ - { + if (environment.tlsOptions.enabled) { + protocol = 'https'; + } + + environmentsInfo.push({ protocol, - dataFile: userFlags.data, + dataFile, name: environment.name, hostname: environment.hostname, port: environment.port, endpointPrefix: environment.endpointPrefix, initialDataDir: null, logTransaction: userFlags['log-transaction'] - } - ]; - } - - private async getEnvInfoListFromNonContainerFlag( - userFlags - ): Promise { - const environments = await parseDataFile(userFlags.data); - - if (userFlags.all) { - return this.getEnvInfoFromEnvironments(userFlags, environments); + }); } - return this.getEnvInfoFromUserChoice(userFlags, environments); + return environmentsInfo; } - private async getEnvInfoFromEnvironments( - userFlags, + private async getEnvironmentsInfo( + userFlags: StartFlags, environments: Environments ): Promise { - const environmentInfoList: EnvironmentInfo[] = []; + const environmentsInfo: EnvironmentInfo[] = []; for (let envIndex = 0; envIndex < environments.length; envIndex++) { try { - const environmentInfo = await prepareData({ - environments, - options: { - index: envIndex, - name: environments[envIndex].name, - port: environments[envIndex].port, - endpointPrefix: environments[envIndex].endpointPrefix + const environmentInfo = await prepareEnvironment({ + environment: environments[envIndex], + userOptions: { + hostname: userFlags.hostname[envIndex], + pname: userFlags.pname[envIndex], + port: userFlags.port[envIndex] }, repair: userFlags.repair }); - environmentInfoList.push({ + environmentsInfo.push({ ...environmentInfo, - initialDataDir: getDirname(userFlags.data), + initialDataDir: getDirname(userFlags.data[envIndex]), logTransaction: userFlags['log-transaction'] }); } catch (error: any) { @@ -279,40 +278,7 @@ export default class Start extends Command { } } - return environmentInfoList; - } - - private async getEnvInfoFromUserChoice( - userFlags, - environments: Environments - ): Promise { - userFlags = await promptEnvironmentChoice(userFlags, environments); - - let environmentInfo: EnvironmentInfo; - - try { - environmentInfo = await prepareData({ - environments, - options: { - index: userFlags.index, - name: userFlags.name, - port: userFlags.port, - hostname: userFlags.hostname, - pname: userFlags.pname - }, - repair: userFlags.repair - }); - } catch (error: any) { - this.error(error.message); - } - - return [ - { - ...environmentInfo, - initialDataDir: getDirname(userFlags.data), - logTransaction: userFlags['log-transaction'] - } - ]; + return environmentsInfo; } private async validateName(name: string) { diff --git a/src/constants/command.constants.ts b/src/constants/command.constants.ts index b2176ce..ab6461a 100644 --- a/src/constants/command.constants.ts +++ b/src/constants/command.constants.ts @@ -7,13 +7,15 @@ export const commonFlags = { export const startFlags = { data: flags.string({ char: 'd', - description: 'Path or URL to your Mockoon data file', - required: true + description: 'Path(s) or URL(s) to your Mockoon data file(s)', + required: true, + multiple: true }), - port: flags.integer({ char: 'p', - description: "Override environment's port" + description: 'Override environment(s) port(s)', + multiple: true, + default: [] }), 'log-transaction': flags.boolean({ char: 't', @@ -25,22 +27,5 @@ export const startFlags = { description: 'If the data file seems too old, or an invalid Mockoon file, migrate/repair without prompting', default: false - }), - name: flags.string({ - char: 'n', - description: - '[deprecated] Select by environment name in the legacy export file', - exclusive: ['index'] - }), - index: flags.integer({ - char: 'i', - description: - "[deprecated] Select by environment's index in the legacy export file", - exclusive: ['name'] - }), - all: flags.boolean({ - char: 'a', - description: '[deprecated] Run all environments in the legacy export file', - exclusive: ['name', 'index'] }) }; diff --git a/src/constants/docker.constants.ts b/src/constants/docker.constants.ts index cc6667e..35a7daf 100644 --- a/src/constants/docker.constants.ts +++ b/src/constants/docker.constants.ts @@ -1,15 +1,17 @@ export const DOCKER_TEMPLATE = `FROM node:14-alpine RUN npm install -g @mockoon/cli@{{{version}}} -COPY {{{filePath}}} ./data +{{#filePaths}} +COPY {{{.}}} ./{{{.}}} +{{/filePaths}} # Do not run as root. RUN adduser --shell /bin/sh --disabled-password --gecos "" mockoon RUN chown -R mockoon ./data USER mockoon -EXPOSE {{{port}}} +EXPOSE{{#ports}} {{.}}{{/ports}} -ENTRYPOINT ["mockoon-cli", "start", "--hostname", "0.0.0.0", "--daemon-off", "--data", "data", "--container"{{{args}}}] +ENTRYPOINT ["mockoon-cli", "start", "--hostname", "0.0.0.0", "--daemon-off", "--data", {{#filePaths}}"{{.}}", {{/filePaths}}"--container"{{{args}}}] # Usage: docker run -p : mockoon-test`; diff --git a/src/constants/messages.constants.ts b/src/constants/messages.constants.ts index 25bf59e..0edb49a 100644 --- a/src/constants/messages.constants.ts +++ b/src/constants/messages.constants.ts @@ -8,7 +8,7 @@ export const Messages = { PROCESS_STARTED: 'Mock started at %s://%s:%d (pid: %d, name: %s)', DOCKERIZE_SUCCESS: 'Dockerfile was generated and saved to %s', DOCKERIZE_BUILD_COMMAND: - 'Run the following commands to build the image and run the container:\n cd %s\n docker build -t %s .\n docker run -d -p %d:%d %s', + 'Run the following commands to build the image and run the container:\n cd %s\n docker build -t %s .\n docker run -d %s %s', PROCESS_NAME_USED_ERROR: 'A process with the name "%s" is already running\nChange the environment\'s name in the data file or run start command with the "--pname" flag', PROCESS_START_LOG_ERROR: `Cannot start %s due to errors (see errors in ${join( @@ -23,9 +23,6 @@ export const Messages = { "These environment's data are too old or not a valid Mockoon environment.\nPlease verify or migrate them using a more recent version of the application", DATA_TOO_RECENT_ERROR: "These environment's data are too recent and cannot be run with the CLI\nPlease update the CLI with the following command 'npm install -g @mockoon/cli'", - ENVIRONMENT_NOT_FOUND_INDEX_ERROR: 'Environment not found at index "%d"', - ENVIRONMENT_NOT_FOUND_NAME_ERROR: - 'Environment with name "%s" cannot be found', ENVIRONMENT_NOT_AVAILABLE_ERROR: 'No environments exist in specified file', PORT_ALREADY_USED: 'Port "%d" is already in use\nChange the environment\'s port in the data file or run start command with the "--port" flag', diff --git a/src/libs/data.ts b/src/libs/data.ts index 27c945c..b418fce 100644 --- a/src/libs/data.ts +++ b/src/libs/data.ts @@ -14,7 +14,6 @@ import { prompt } from 'inquirer'; import * as mkdirp from 'mkdirp'; import { join } from 'path'; import { ProcessDescription } from 'pm2'; -import { format } from 'util'; import { Config } from '../config'; import { Messages } from '../constants/messages.constants'; import { transformEnvironmentName } from './utils'; @@ -22,51 +21,71 @@ import { transformEnvironmentName } from './utils'; /** * Load and parse a JSON data file. * Supports both legacy export files (with one or multiple envs) or new environment files. + * If a legacy export is encountered, unwrap it and update `--data` flag to reflect the number of environments unwrapped * * @param filePath */ -export const parseDataFile = async ( - filePath: string -): Promise => { +export const parseDataFiles = async ( + filePaths: string[] +): Promise<{ filePaths: string[]; environments: Environments }> => { const openAPIConverter = new OpenAPIConverter(); let environments: Environments = []; + let newFilePaths: string[] = []; - try { - const environment = await openAPIConverter.convertFromOpenAPI(filePath); + let filePathIndex = 0; - if (environment) { - environments.push(environment); - } - } catch (openAPIError: any) { + for (const filePath of filePaths) { try { - let data: any; - - if (filePath.startsWith('http')) { - data = (await axios.get(filePath, { timeout: 30000 })).data; - } else { - data = await fs.readFile(filePath, { encoding: 'utf-8' }); - } + const environment = await openAPIConverter.convertFromOpenAPI(filePath); - if (typeof data === 'string') { - data = JSON.parse(data); + if (environment) { + environments.push(environment); + newFilePaths.push(filePath); } - - if (IsLegacyExportData(data)) { - // Extract all environments, eventually filter items of type 'route' - environments = UnwrapLegacyExport(data); - } else if (typeof data === 'object') { - environments.push(data); + } catch (openAPIError: any) { + try { + let data: any; + + if (filePath.startsWith('http')) { + data = (await axios.get(filePath, { timeout: 30000 })).data; + } else { + data = await fs.readFile(filePath, { encoding: 'utf-8' }); + } + + if (typeof data === 'string') { + data = JSON.parse(data); + } + + if (IsLegacyExportData(data)) { + const unwrappedExport = UnwrapLegacyExport(data); + + // Extract all environments, eventually filter items of type 'route' + environments = [...environments, ...unwrappedExport]; + + // if we unwrapped more than one exported environment, add as many `--data` flag entries + if (unwrappedExport.length >= 1) { + newFilePaths = [ + ...newFilePaths, + ...new Array(unwrappedExport.length).fill(filePath) + ]; + } + } else if (typeof data === 'object') { + environments.push(data); + newFilePaths.push(filePath); + } + } catch (JSONError: any) { + throw new Error(`${Messages.CLI.DATA_INVALID}: ${JSONError.message}`); } - } catch (JSONError: any) { - throw new Error(`${Messages.CLI.DATA_INVALID}: ${JSONError.message}`); } + + filePathIndex++; } if (environments.length === 0) { throw new Error(Messages.CLI.ENVIRONMENT_NOT_AVAILABLE_ERROR); } - return environments; + return { filePaths: newFilePaths, environments }; }; /** @@ -78,7 +97,6 @@ export const parseDataFile = async ( */ const migrateAndValidateEnvironment = async ( environment: Environment, - environmentName: string | undefined, forceRepair?: boolean ) => { // environment data are too old: lastMigration is not present @@ -87,7 +105,7 @@ const migrateAndValidateEnvironment = async ( { name: 'repair', message: `${ - environmentName ? '"' + environmentName + '"' : 'This environment' + environment.name ? '"' + environment.name + '"' : 'This environment' } does not seem to be a valid Mockoon environment or is too old. Let Mockoon attempt to repair it?`, type: 'confirm', default: true @@ -121,63 +139,18 @@ const migrateAndValidateEnvironment = async ( }; /** - * Check if data file is in the new format (with data and source) - * and return the environment by index or name - * - * @param data - * @param options - */ -const getEnvironment = ( - environments: Environments, - options: { name?: string; index?: number } -): Environment => { - let findError: string; - - // find environment by index - if (options.index !== undefined && environments[options.index]) { - return environments[options.index]; - } else { - findError = format( - Messages.CLI.ENVIRONMENT_NOT_FOUND_INDEX_ERROR, - options.index - ); - } - - // find by name - if (options.name) { - const foundEnvironment = environments.find( - (environment) => environment.name === options.name - ); - - if (foundEnvironment) { - return foundEnvironment; - } else { - findError = format( - Messages.CLI.ENVIRONMENT_NOT_FOUND_NAME_ERROR, - options.name - ); - } - } - - throw new Error(findError); -}; - -/** - * Load the data file, find and migrate the environment - * copy the environment to a new temporary file. + * Migrate the environment + * Copy the environment to a new temporary file. * * @param environments - path to the data file or export data * @param options */ -export const prepareData = async (parameters: { - environments: Environments; - options: { - name?: string; - index?: number; +export const prepareEnvironment = async (params: { + environment: Environment; + userOptions: { port?: number; pname?: string; hostname?: string; - endpointPrefix?: string; }; dockerfileDir?: string; repair?: boolean; @@ -189,51 +162,44 @@ export const prepareData = async (parameters: { port: number; dataFile: string; }> => { - let environment: Environment = getEnvironment( - parameters.environments, - parameters.options - ); - - environment = await migrateAndValidateEnvironment( - environment, - parameters.options.name, - parameters.repair + params.environment = await migrateAndValidateEnvironment( + params.environment, + params.repair ); // transform the provided name or env's name to be used as process name - environment.name = transformEnvironmentName( - parameters.options.pname || environment.name + params.environment.name = transformEnvironmentName( + params.userOptions.pname || params.environment.name ); - if (parameters.options.port !== undefined) { - environment.port = parameters.options.port; + if (params.userOptions.port !== undefined) { + params.environment.port = params.userOptions.port; } - if (parameters.options.hostname !== undefined) { - environment.hostname = parameters.options.hostname; + if (params.userOptions.hostname !== undefined) { + params.environment.hostname = params.userOptions.hostname; } - if (parameters.options.endpointPrefix !== undefined) { - environment.endpointPrefix = parameters.options.endpointPrefix; - } - - let dataFile: string = join(Config.dataPath, `${environment.name}.json`); + let dataFile: string = join( + Config.dataPath, + `${params.environment.name}.json` + ); // if we are building a Dockerfile, we want the data in the same folder - if (parameters.dockerfileDir) { - await mkdirp(parameters.dockerfileDir); - dataFile = `${parameters.dockerfileDir}/${environment.name}.json`; + if (params.dockerfileDir) { + await mkdirp(params.dockerfileDir); + dataFile = `${params.dockerfileDir}/${params.environment.name}.json`; } // save environment to data path - await fs.writeFile(dataFile, JSON.stringify(environment)); + await fs.writeFile(dataFile, JSON.stringify(params.environment)); return { - name: environment.name, - protocol: environment.tlsOptions.enabled ? 'https' : 'http', - hostname: environment.hostname, - port: environment.port, - endpointPrefix: environment.endpointPrefix, + name: params.environment.name, + protocol: params.environment.tlsOptions.enabled ? 'https' : 'http', + hostname: params.environment.hostname, + port: params.environment.port, + endpointPrefix: params.environment.endpointPrefix, dataFile }; }; diff --git a/src/libs/server.ts b/src/libs/server.ts index 0e1b0e5..57cd1e3 100644 --- a/src/libs/server.ts +++ b/src/libs/server.ts @@ -48,8 +48,10 @@ const addEventListeners = function ( environment: Environment, logTransaction?: boolean ) { + const logMeta: any = { mockName: environment.name }; + server.on('started', () => { - logger.info(format(Messages.SERVER.STARTED, environment.port)); + logger.info(format(Messages.SERVER.STARTED, environment.port), logMeta); if (!!process.send) { process.send('ready'); @@ -77,30 +79,37 @@ const addEventListeners = function ( ServerErrorCodes.PROXY_ERROR ].indexOf(errorCode) > -1 ) { - logger.error(error?.message); + logger.error(error?.message || '', logMeta); } }); server.on('creating-proxy', () => { - logger.info(format(Messages.SERVER.CREATING_PROXY, environment.proxyHost)); + logger.info( + format(Messages.SERVER.CREATING_PROXY, environment.proxyHost), + logMeta + ); }); server.on('transaction-complete', (transaction: Transaction) => { transaction.request.method = transaction.request.method.toUpperCase() as keyof typeof Methods; + if (logTransaction) { + logMeta.transaction = transaction; + } + logger.info( `${transaction.request.method.toUpperCase()} ${ transaction.request.urlPath } | ${transaction.response.statusCode}${ transaction.proxied ? ' | proxied' : '' }`, - logTransaction ? { transaction } : {} + logMeta ); }); server.on('stopped', () => { - logger.info(Messages.SERVER.STOPPED); + logger.info(Messages.SERVER.STOPPED, logMeta); }); process.on('SIGINT', () => { diff --git a/src/libs/utils.ts b/src/libs/utils.ts index 442b778..05e6aa7 100644 --- a/src/libs/utils.ts +++ b/src/libs/utils.ts @@ -1,6 +1,4 @@ -import { Environments } from '@mockoon/commons'; import { cli } from 'cli-ux'; -import { prompt } from 'inquirer'; import * as isPortReachable from 'is-port-reachable'; import { dirname } from 'path'; import { ProcessDescription } from 'pm2'; @@ -99,43 +97,6 @@ export const portInUse = async ( */ export const portIsValid = (port: number): boolean => port >= 0 && port < 65536; -/** - * Check if --index or --name flag are provided and - * prompt user to choose an environment if not. - * If there is only one environment, launch it by default - * - * @param flags - * @param environments - */ -export const promptEnvironmentChoice = async < - T extends { index: number | undefined; name: string | undefined } ->( - flags: T, - environments: Environments -): Promise => { - if (flags.index === undefined && !flags.name) { - if (environments.length === 1) { - flags.index = 0; - } else { - const response: { environmentIndex: number } = await prompt([ - { - name: 'environmentIndex', - message: 'Please select an environment', - type: 'list', - choices: environments.map((environment, environmentIndex) => ({ - name: environment.name || environmentIndex, - value: environmentIndex - })) - } - ]); - - flags.index = response.environmentIndex; - } - } - - return flags; -}; - /** * Get the path directory, except if it's a URL. * diff --git a/test/specs/dockerize-command.spec.ts b/test/specs/dockerize-command.spec.ts index 18f958e..8975ae1 100644 --- a/test/specs/dockerize-command.spec.ts +++ b/test/specs/dockerize-command.spec.ts @@ -6,47 +6,123 @@ import { readFile as readJSONFile } from 'jsonfile'; import { Config } from '../../src/config'; describe('Dockerize command', () => { - test - .stdout() - .command([ - 'dockerize', - '--data', - './test/data/envs/mock1.json', - '--port', - '3010', - '--output', - './tmp/Dockerfile' - ]) - .it('should successfully run the command', (context) => { - expect(context.stdout).to.contain( - 'Dockerfile was generated and saved to /home/runner/work/cli/cli/tmp/Dockerfile' - ); - expect(context.stdout).to.contain('cd /home/runner/work/cli/cli/tmp'); - expect(context.stdout).to.contain('docker build -t mockoon-mock1 .'); - expect(context.stdout).to.contain( - 'docker run -d -p 3010:3010 mockoon-mock1' - ); - }); + describe('Dockerize single mock', () => { + test + .stdout() + .command([ + 'dockerize', + '--data', + './test/data/envs/mock1.json', + '--port', + '3010', + '--output', + './tmp/Dockerfile' + ]) + .it('should successfully run the command', (context) => { + expect(context.stdout).to.contain( + 'Dockerfile was generated and saved to /home/runner/work/cli/cli/tmp/Dockerfile' + ); + expect(context.stdout).to.contain('cd /home/runner/work/cli/cli/tmp'); + expect(context.stdout).to.contain('docker build -t mockoon-mock1 .'); + expect(context.stdout).to.contain( + 'docker run -d -p 3010:3010 mockoon-mock1' + ); + }); - test.it( - 'should generate the Dockerfile with the correct content', - async () => { - const dockerfile = await fs.readFile('./tmp/Dockerfile'); - const dockerfileContent = dockerfile.toString(); - expect(dockerfileContent).to.contain( - `RUN npm install -g @mockoon/cli@${Config.version}` - ); - expect(dockerfileContent).to.contain('COPY mockoon-mock1.json ./data'); - expect(dockerfileContent).to.contain('EXPOSE 3010'); - } - ); + test.it( + 'should generate the Dockerfile with the correct content', + async () => { + const dockerfile = await fs.readFile('./tmp/Dockerfile'); + const dockerfileContent = dockerfile.toString(); + expect(dockerfileContent).to.contain( + `RUN npm install -g @mockoon/cli@${Config.version}` + ); + expect(dockerfileContent).to.contain( + 'COPY mockoon-mock1.json ./mockoon-mock1.json' + ); + expect(dockerfileContent).to.contain( + 'ENTRYPOINT ["mockoon-cli", "start", "--hostname", "0.0.0.0", "--daemon-off", "--data", "mockoon-mock1.json", "--container"]' + ); + expect(dockerfileContent).to.contain('EXPOSE 3010'); + } + ); + + test.it( + 'should generate mock JSON file next to the Dockerfile', + async () => { + const mockFile: Environment = await readJSONFile( + './tmp/mockoon-mock1.json', + 'utf-8' + ); + expect(mockFile.name).to.equal('mockoon-mock1'); + expect(mockFile.port).to.equal(3010); + } + ); + }); + + describe('Dockerize multiple mocks', () => { + test + .stdout() + .command([ + 'dockerize', + '--data', + './test/data/envs/mock1.json', + './test/data/envs/mock2.json', + '--port', + '3010', + '3011', + '--output', + './tmp/Dockerfile' + ]) + .it('should successfully run the command', (context) => { + expect(context.stdout).to.contain( + 'Dockerfile was generated and saved to /home/runner/work/cli/cli/tmp/Dockerfile' + ); + expect(context.stdout).to.contain('cd /home/runner/work/cli/cli/tmp'); + expect(context.stdout).to.contain('docker build -t mockoon-mocks .'); + expect(context.stdout).to.contain( + 'docker run -d -p 3010:3010 -p 3011:3011 mockoon-mocks' + ); + }); + + test.it( + 'should generate the Dockerfile with the correct content', + async () => { + const dockerfile = await fs.readFile('./tmp/Dockerfile'); + const dockerfileContent = dockerfile.toString(); + expect(dockerfileContent).to.contain( + `RUN npm install -g @mockoon/cli@${Config.version}` + ); + expect(dockerfileContent).to.contain( + 'COPY mockoon-mock1.json ./mockoon-mock1.json' + ); + expect(dockerfileContent).to.contain( + 'COPY mockoon-mock2.json ./mockoon-mock2.json' + ); + expect(dockerfileContent).to.contain( + 'ENTRYPOINT ["mockoon-cli", "start", "--hostname", "0.0.0.0", "--daemon-off", "--data", "mockoon-mock1.json", "mockoon-mock2.json", "--container"]' + ); + expect(dockerfileContent).to.contain('EXPOSE 3010 3011'); + } + ); + + test.it( + 'should generate mock JSON file next to the Dockerfile', + async () => { + const mockFile1: Environment = await readJSONFile( + './tmp/mockoon-mock1.json', + 'utf-8' + ); + expect(mockFile1.name).to.equal('mockoon-mock1'); + expect(mockFile1.port).to.equal(3010); - test.it('should generate mock JSON file next to the Dockerfile', async () => { - const mockFile: Environment = await readJSONFile( - './tmp/mockoon-mock1.json', - 'utf-8' + const mockFile2: Environment = await readJSONFile( + './tmp/mockoon-mock2.json', + 'utf-8' + ); + expect(mockFile2.name).to.equal('mockoon-mock2'); + expect(mockFile2.port).to.equal(3011); + } ); - expect(mockFile.name).to.equal('mockoon-mock1'); - expect(mockFile.port).to.equal(3010); }); }); diff --git a/test/specs/legacy-export-file.spec.ts b/test/specs/legacy-export-file.spec.ts index a9b0c32..944fd34 100644 --- a/test/specs/legacy-export-file.spec.ts +++ b/test/specs/legacy-export-file.spec.ts @@ -6,13 +6,7 @@ import { stopProcesses } from '../libs/helpers'; describe('Legacy export file', () => { test .stderr() - .command([ - 'start', - '--data', - './test/data/legacy-export-file/empty.json', - '-i', - '0' - ]) + .command(['start', '--data', './test/data/legacy-export-file/empty.json']) .catch((context) => { expect(context.message).to.contain( 'No environments exist in specified file' @@ -20,44 +14,9 @@ describe('Legacy export file', () => { }) .it('should fail when file contains no environment'); - test - .stderr() - .command([ - 'start', - '--data', - './test/data/legacy-export-file/export.json', - '-i', - '99' - ]) - .catch((context) => { - expect(context.message).to.contain('Environment not found at index "99"'); - }) - .it('should fail when there is no environment at index'); - - test - .stderr() - .command([ - 'start', - '--data', - './test/data/legacy-export-file/export.json', - '-n', - 'non-existing-environment-name' - ]) - .catch((context) => { - expect(context.message).to.contain( - 'Environment with name "non-existing-environment-name" cannot be found' - ); - }) - .it('should fail when there is no environment with requested name'); - test .stdout() - .command([ - 'start', - '--all', - '--data', - './test/data/legacy-export-file/multi.json' - ]) + .command(['start', '--data', './test/data/legacy-export-file/multi.json']) .it('should start all envs', (context) => { expect(context.stdout).to.contain( 'Mock started at http://localhost:3000 (pid: 0, name: mockoon-env0-mock0)' diff --git a/test/specs/run-multiple-mocks.spec.ts b/test/specs/run-multiple-mocks.spec.ts index 75397dc..0e7b6f4 100644 --- a/test/specs/run-multiple-mocks.spec.ts +++ b/test/specs/run-multiple-mocks.spec.ts @@ -24,40 +24,39 @@ describe('Run two mocks on the same port', () => { stopProcesses('all', ['mockoon-mock1']); }); -describe('Run two mocks on different port', () => { - test - .stdout() - .command(['start', '--data', './test/data/envs/mock1.json']) - .it('should start first mock on port 3000', (context) => { - expect(context.stdout).to.contain( - 'Mock started at http://localhost:3000 (pid: 0, name: mockoon-mock1)' - ); - }); - +describe('Run two mocks on different ports, with different process names', () => { test .stdout() .command([ 'start', '--data', + './test/data/envs/mock1.json', './test/data/envs/mock2.json', '--port', - '3001' + '3005', + '3006', + '--pname', + 'pname1', + 'pname2' ]) - .it('should start second mock on port 3001', (context) => { + .it('should start first mock on port 3000', (context) => { + expect(context.stdout).to.contain( + 'Mock started at http://localhost:3005 (pid: 0, name: mockoon-pname1)' + ); expect(context.stdout).to.contain( - 'Mock started at http://localhost:3001 (pid: 1, name: mockoon-mock2)' + 'Mock started at http://localhost:3006 (pid: 1, name: mockoon-pname2)' ); }); test.it('should call GET /api/test endpoint and get a result', async () => { - const call1 = await axios.get('http://localhost:3000/api/test'); - const call2 = await axios.get('http://localhost:3001/api/test'); + const call1 = await axios.get('http://localhost:3005/api/test'); + const call2 = await axios.get('http://localhost:3006/api/test'); expect(call1.data).to.contain('mock-content-1'); expect(call2.data).to.contain('mock-content-2'); }); - stopProcesses('all', ['mockoon-mock1', 'mockoon-mock2']); + stopProcesses('all', ['mockoon-pname1', 'mockoon-pname2']); }); describe('Run two mocks with same name', () => {