@elastic/synthetics
Version:
Elastic synthetic monitoring agent
377 lines (362 loc) • 11.4 kB
text/typescript
/**
* MIT License
*
* Copyright (c) 2020-present, Elastic NV
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
*/
import { program, Option } from 'commander';
import { cwd } from 'process';
import { bold } from 'kleur/colors';
import { resolve } from 'path';
import { CliArgs, PushOptions, RunOptions } from './common_types';
import { reporters } from './reporters';
import {
normalizeOptions,
parseThrottling,
getCommonCommandOpts,
collectOpts,
parseFileOption,
} from './options';
import { globalSetup } from './loader';
import { run } from './';
import { runner } from './core/globals';
import { SyntheticsLocations } from './dsl/monitor';
import { push, loadSettings, validatePush, warnIfThrottled } from './push';
import {
formatLocations,
getLocations,
renderLocations,
LocationCmdOptions,
} from './locations';
import { Generator } from './generator';
import { error, write } from './helpers';
import { LocationsMap } from './locations/public-locations';
import { createLightweightMonitors } from './push/monitor';
import { getVersion } from './push/kibana_api';
import { installTransform } from './core/transform';
import { totp, TOTPCmdOptions } from './core/mfa';
import { setGlobalProxy } from './helpers';
/* eslint-disable-next-line @typescript-eslint/no-var-requires */
const { name, version } = require('../package.json');
const proxySettings = {};
const {
params,
pattern,
playwrightOpts,
auth,
authMandatory,
configOpt,
tags,
match,
fields,
maintenanceWindows,
} = getCommonCommandOpts();
program
.name(`npx ${name}`)
.usage('[options] [dir] [files] file')
.addOption(configOpt)
.addOption(pattern)
.addOption(tags)
.addOption(match)
.addOption(params)
.addOption(
new Option('--reporter <value>', `output reporter format`).choices(
Object.keys(reporters)
)
)
.option('--inline', 'read journeys from stdin instead of reading from files')
.option('-r, --require <modules...>', 'module(s) to preload')
.option('--sandbox', 'enable chromium sand-boxing')
.option(
'--rich-events',
'preset flag used when running monitors directly via Heartbeat'
)
.option('--no-headless', 'run with the browser in headful mode')
.option(
'--capability <features...>',
'Enable capabilities through feature flags'
)
.addOption(
new Option(
'--screenshots [flag]',
'Control whether to capture screenshots at the end of each step'
).choices(['on', 'off', 'only-on-failure'])
)
.option(
'--dry-run',
"don't actually execute anything, report only registered journeys"
)
.option(
'--outfd <fd>',
'specify a file descriptor for logs. Default is stdout',
parseInt
)
.option(
'--ws-endpoint <endpoint>',
'Browser WebSocket endpoint to connect to'
)
.option(
'--pause-on-error',
'pause on error until a keypress is made in the console. Useful during development'
)
.option(
'--ignore-https-errors',
'ignores any HTTPS errors in sites being tested, including ones related to unrecognized certs or signatures. This can be insecure!'
)
.option(
'--quiet-exit-code',
'always return 0 as an exit code status, regardless of test pass / fail. Only return > 0 exit codes on internal errors where the suite could not be run'
)
.option(
'--throttling <config>',
'JSON object to throttle network conditions for download and upload throughput in megabits/second and latency in milliseconds. Ex: { "download": 10, "upload": 5, "latency": 200 }.',
parseThrottling
)
.option(
'--no-throttling',
'Turns off default network throttling.',
parseThrottling
)
.addOption(playwrightOpts)
.version(version)
.description('Run synthetic tests')
.action(async (cliArgs: CliArgs) => {
const tearDown = await globalSetup(cliArgs, program.args);
try {
const options = await normalizeOptions(cliArgs, 'run');
const results = await run(options);
/**
* Exit with error status if any journey fails
*/
if (!options.quietExitCode) {
for (const result of Object.values(results)) {
if (result.status === 'failed') {
process.exit(1);
}
}
}
} catch (e) {
console.error(e);
process.exit(1);
} finally {
tearDown();
}
});
// Push command
program
.command('push')
.description(
'Push all journeys in the current directory to create monitors within the Kibana monitor management UI'
)
.addOption(authMandatory)
.option(
'--schedule <time-in-minutes>',
"schedule in minutes for the pushed monitors. Setting `10`, for example, configures monitors which don't have an interval defined to run every 10 minutes.",
parseInt
)
.addOption(
new Option(
'--locations <locations...>',
'default list of locations from which your monitors will run.'
).choices(SyntheticsLocations)
)
.option(
'--private-locations <locations...>',
'default list of private locations from which your monitors will run.'
)
.option('--url <url>', 'Kibana URL to upload the project monitors')
.option(
'--id <id>',
'project id that will be used for logically grouping monitors'
)
.option(
'--space <space>',
'the target Kibana spaces for the pushed monitors — spaces help you organise pushed monitors.'
)
.option('-y, --yes', 'skip all questions and run non-interactively')
.option(
'--proxy-uri <uri>',
'proxy uri to use when pushing to kibana',
collectOpts('uri', proxySettings)
)
.option(
'--proxy-token <token>',
'auth token to use the proxy',
collectOpts('token', proxySettings)
)
.option(
'--proxy-ca <path>',
'provide a CA override for proxy endpoint, as a path to be loaded or as string',
collectOpts('ca', proxySettings, parseFileOption)
)
.option(
'--proxy-cert <path>',
'provide a cert override for proxy endpoint, as a path to be loaded or as string',
collectOpts('cert', proxySettings, parseFileOption)
)
.option(
'--proxy-no-verify',
'disable TLS verification for the proxy connection',
collectOpts('noVerify', proxySettings),
false
)
.addOption(pattern)
.addOption(tags)
.addOption(fields)
.addOption(match)
.addOption(params)
.addOption(playwrightOpts)
.addOption(configOpt)
.addOption(maintenanceWindows)
.action(async cmdOpts => {
cmdOpts = { ...cmdOpts, ...program.opts() };
const workDir = cwd();
const tearDown = await globalSetup({ inline: false, ...cmdOpts }, [
workDir,
]);
try {
const settings = await loadSettings(cmdOpts.config);
const options = (await normalizeOptions(
{
...settings,
...cmdOpts,
...{
proxy: proxySettings,
},
},
'push'
)) as PushOptions;
//Set up global proxy agent if any of the related options are set
setGlobalProxy(options.proxy ?? {});
await validatePush(options, settings);
const monitors = runner._buildMonitors(options);
if ((options as CliArgs).throttling == null) {
warnIfThrottled(monitors);
}
options.kibanaVersion = await getVersion(options);
monitors.push(...(await createLightweightMonitors(workDir, options)));
await push(monitors, options);
} catch (e) {
e && console.error(e);
process.exit(1);
} finally {
tearDown();
}
});
// Init command
program
.command('init [dir]')
.description('Initialize Elastic synthetics project')
.action(async (dir = '') => {
try {
const generator = await new Generator(resolve(process.cwd(), dir));
await generator.setup();
} catch (e) {
e && error(e);
process.exit(1);
}
});
// Locations command
program
.command('locations')
.description(
`List all locations to run the synthetics monitors. Pass optional '--url' and '--auth' to list private locations.`
)
.option('--url <url>', 'Kibana URL to fetch all public and private locations')
.addOption(auth)
.option(
'--proxy-uri <uri>',
'proxy uri to use when pushing to kibana',
collectOpts('uri', proxySettings)
)
.option(
'--proxy-token <token>',
'auth token to use the proxy',
collectOpts('token', proxySettings)
)
.option(
'--proxy-ca <path>',
'provide a CA override for proxy endpoint, as a path to be loaded or as string',
collectOpts('ca', proxySettings, parseFileOption('--proxy-ca'))
)
.option(
'--proxy-cert <path>',
'provide a cert override for proxy endpoint, as a path to be loaded or as string',
collectOpts('cert', proxySettings, parseFileOption('--proxy-cert'))
)
.option(
'--proxy-no-verify',
'disable TLS verification for the proxy connection',
collectOpts('noVerify', proxySettings),
false
)
.action(async (cmdOpts: LocationCmdOptions) => {
const revert = installTransform();
const url = cmdOpts.url ?? (await loadSettings(null, true))?.url;
try {
const settings = await loadSettings(null, true);
const options = (await normalizeOptions({
...settings,
...cmdOpts,
...{
proxy: proxySettings,
},
})) as RunOptions;
//Set up global proxy agent if any of the related options are set
setGlobalProxy(options.proxy ?? {});
if (url && cmdOpts.auth) {
const allLocations = await getLocations({
url,
auth: cmdOpts.auth,
});
renderLocations(formatLocations(allLocations));
} else {
renderLocations({ publicLocations: Object.keys(LocationsMap) });
}
} catch (e) {
e && error(e);
process.exit(1);
} finally {
revert();
}
});
// TOTP command
program
.command('totp <secret>')
.description(
'Generate a Time-based One-Time token using the provided secret.'
)
.option(
'--issuer <issuer>',
'Provider or Service the secret is associated with.'
)
.option('--label <label>', 'Account Identifier (default: SyntheticsTOTP)')
.action((secret, cmdOpts: TOTPCmdOptions) => {
try {
const token = totp(secret, cmdOpts);
write(bold(`OTP Token: ${token}`));
} catch (e) {
error(e);
process.exit(1);
}
});
program.parse(process.argv);