From a4c10b567ac6e5a089dd8ae7cc5e02dbf11731b7 Mon Sep 17 00:00:00 2001 From: Vignesh Shanmugam Date: Thu, 2 Jun 2022 17:10:16 -0700 Subject: [PATCH] feat: interactive scaffolding for picking default monitor config (#508) * feat: allow users to specify defaults * add tests and throw error * add tests for push * move validation out * rebase and cleanup * apply suggestions from code review Co-authored-by: Andrew Cholakian Co-authored-by: Andrew Cholakian --- __tests__/core/runner.test.ts | 14 ++++--- __tests__/generator/index.test.ts | 17 +++++++- __tests__/options.test.ts | 3 -- __tests__/push/index.test.ts | 67 +++++++++++++++++++++++++++++++ __tests__/utils/test-config.ts | 4 +- package-lock.json | 7 +--- package.json | 1 + src/cli.ts | 34 +++++++++++----- src/generator/index.ts | 40 ++++++++++++++++-- src/generator/utils.ts | 13 ++++++ src/options.ts | 28 ++++--------- templates/synthetics.config.ts | 5 ++- 12 files changed, 181 insertions(+), 52 deletions(-) create mode 100644 __tests__/push/index.test.ts diff --git a/__tests__/core/runner.test.ts b/__tests__/core/runner.test.ts index 8d564a02..1f731a4d 100644 --- a/__tests__/core/runner.test.ts +++ b/__tests__/core/runner.test.ts @@ -29,10 +29,13 @@ import Runner from '../../src/core/runner'; import { step, journey } from '../../src/core'; import { Journey, Step } from '../../src/dsl'; import { Server } from '../utils/server'; -import { generateTempPath, noop } from '../../src/helpers'; +import { + DEFAULT_THROTTLING_OPTIONS, + generateTempPath, + noop, +} from '../../src/helpers'; import { wsEndpoint } from '../utils/test-config'; import { Reporter } from '../../src/reporters'; -import { getDefaultMonitorConfig } from '../../src/options'; import { JourneyEndResult, JourneyStartResult, @@ -686,7 +689,7 @@ describe('runner', () => { runner.addJourney(j2); const monitors = runner.buildMonitors({ - ...getDefaultMonitorConfig(), + throttling: DEFAULT_THROTTLING_OPTIONS, }); expect(monitors.length).toBe(2); expect(monitors[0].config).toEqual({ @@ -700,8 +703,6 @@ describe('runner', () => { throttling: { download: 5, latency: 20, upload: 3 }, }); expect(monitors[1].config).toMatchObject({ - locations: ['us_east'], - schedule: 10, throttling: { latency: 1000 }, }); }); @@ -709,6 +710,7 @@ describe('runner', () => { it('runner - build monitors with global config', async () => { runner.updateMonitor({ schedule: 5, + locations: ['us_east'], throttling: { download: 100, upload: 50 }, params: { env: 'test' }, playwrightOptions: { ignoreHTTPSErrors: true }, @@ -726,7 +728,7 @@ describe('runner', () => { runner.addJourney(j2); const monitors = runner.buildMonitors({ - ...getDefaultMonitorConfig(), + throttling: DEFAULT_THROTTLING_OPTIONS, }); expect(monitors.length).toBe(2); expect(monitors[0].config).toEqual({ diff --git a/__tests__/generator/index.test.ts b/__tests__/generator/index.test.ts index b00d6ea3..e3f2d1a3 100644 --- a/__tests__/generator/index.test.ts +++ b/__tests__/generator/index.test.ts @@ -42,7 +42,15 @@ describe('Generator', () => { }); }); it('generate synthetics project - NPM', async () => { - const cli = new CLIMock().args(['init', scaffoldDir]).run(); + const cli = new CLIMock().args(['init', scaffoldDir]).run({ + env: { + ...process.env, + TEST_QUESTIONS: JSON.stringify({ + locations: 'us_east', + schedule: 30, + }), + }, + }); expect(await cli.exitCode).toBe(0); // Verify files @@ -62,6 +70,13 @@ describe('Generator', () => { existsSync(join(scaffoldDir, 'journeys', 'example.journey.ts')) ).toBeTruthy(); expect(existsSync(join(scaffoldDir, 'synthetics.config.ts'))).toBeTruthy(); + // Verify schedule and locations + const configFile = await readFile( + join(scaffoldDir, 'synthetics.config.ts'), + 'utf-8' + ); + expect(configFile).toContain(`locations: ["us_east"]`); + expect(configFile).toContain(`schedule: 30`); // Verify stdout const stderr = cli.stderr(); diff --git a/__tests__/options.test.ts b/__tests__/options.test.ts index 5d88ae37..2516fe03 100644 --- a/__tests__/options.test.ts +++ b/__tests__/options.test.ts @@ -75,15 +75,12 @@ describe('options', () => { it('normalize monitor configs', () => { expect(normalizeOptions({ throttling: false })).toMatchObject({ - locations: ['us_east'], - schedule: 10, throttling: {}, }); expect( normalizeOptions({ throttling: { download: 50 }, schedule: 2 }) ).toMatchObject({ - locations: ['us_east'], schedule: 2, throttling: { download: 50, diff --git a/__tests__/push/index.test.ts b/__tests__/push/index.test.ts new file mode 100644 index 00000000..054413aa --- /dev/null +++ b/__tests__/push/index.test.ts @@ -0,0 +1,67 @@ +/** + * 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 { join } from 'path'; +import { CLIMock } from '../utils/test-config'; + +const FIXTURES_DIR = join(__dirname, '..', 'fixtures'); + +describe('Push CLI', () => { + const args = [ + '--url', + 'http://localhost:8000', + '--auth', + 'foo', + '--project', + 'test', + ]; + const journeyFile = join(FIXTURES_DIR, 'example.journey.ts'); + it('errors when no journey files are passed', async () => { + const cli = new CLIMock().args(['push', ...args]).run(); + expect(await cli.exitCode).toBe(1); + + expect(cli.stderr()).toContain( + `error: missing required argument 'journeys'` + ); + }); + + it('erorr when schedule option is empty', async () => { + const cli = new CLIMock().args(['push', journeyFile, ...args]).run(); + expect(await cli.exitCode).toBe(1); + + expect(cli.stderr()).toContain( + `Set default schedule in minutes for all monitors` + ); + }); + + it('errors when locations option is empty', async () => { + const cli = new CLIMock() + .args(['push', journeyFile, ...args, '--schedule', '20']) + .run(); + expect(await cli.exitCode).toBe(1); + + expect(cli.stderr()).toContain(`Set default location for all monitors`); + }); +}); diff --git a/__tests__/utils/test-config.ts b/__tests__/utils/test-config.ts index 5e67f847..31f2c48a 100644 --- a/__tests__/utils/test-config.ts +++ b/__tests__/utils/test-config.ts @@ -55,7 +55,7 @@ export class CLIMock { private waitForPromise: () => void; private cliArgs: string[] = []; private stdinStr?: string; - private stderrStr: string; + private stderrStr = ''; exitCode: Promise; constructor(public debug: boolean = false) {} @@ -75,7 +75,7 @@ export class CLIMock { return this; } - run(spawnOverrides?: { cwd?: string }): CLIMock { + run(spawnOverrides?: { cwd?: string; env?: NodeJS.ProcessEnv }): CLIMock { this.process = spawn( 'node', [join(__dirname, '..', '..', 'dist', 'cli.js'), ...this.cliArgs], diff --git a/package-lock.json b/package-lock.json index 69067a01..4ca0ce13 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "archiver": "^5.3.0", "commander": "^9.0.0", "deepmerge": "^4.2.2", + "enquirer": "^2.3.6", "esbuild": "^0.14.27", "expect": "^27.0.2", "http-proxy": "^1.18.1", @@ -1981,7 +1982,6 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", - "dev": true, "engines": { "node": ">=6" } @@ -2991,7 +2991,6 @@ "version": "2.3.6", "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz", "integrity": "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==", - "dev": true, "dependencies": { "ansi-colors": "^4.1.1" }, @@ -9899,8 +9898,7 @@ "ansi-colors": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", - "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", - "dev": true + "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==" }, "ansi-escapes": { "version": "4.3.2", @@ -10659,7 +10657,6 @@ "version": "2.3.6", "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz", "integrity": "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==", - "dev": true, "requires": { "ansi-colors": "^4.1.1" } diff --git a/package.json b/package.json index 5dd5b6e2..6e3c85d2 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "archiver": "^5.3.0", "commander": "^9.0.0", "deepmerge": "^4.2.2", + "enquirer": "^2.3.6", "esbuild": "^0.14.27", "expect": "^27.0.2", "http-proxy": "^1.18.1", diff --git a/src/cli.ts b/src/cli.ts index 44a9cfec..3bc62523 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -25,7 +25,7 @@ * */ -import { program, Option } from 'commander'; +import { program, Option, Argument } from 'commander'; import { CliArgs, PushOptions } from './common_types'; import { reporters } from './reporters'; import { normalizeOptions, parseThrottling } from './options'; @@ -146,9 +146,15 @@ program // Push command program - .command('push [files...]') + .command('push') + .addArgument( + new Argument( + '[journeys...]', + 'file path to journey directory and individual files' + ).argRequired() + ) .description( - 'Push monitors to create new montors with Kibana monitor management UI' + 'Push journeys to create montors within Kibana monitor management UI' ) .option( '--schedule ', @@ -162,8 +168,8 @@ program ).choices(SyntheticsLocations) ) .requiredOption( - '--project ', - 'project/repository that will be used for grouping the monitors.' + '--project ', + 'id that will be used for logically grouping monitors' ) .requiredOption('--url ', 'kibana URL to upload the monitors') .requiredOption( @@ -176,15 +182,23 @@ program 'default' ) .option('--delete', 'automatically delete the stale monitors.') - .action(async (files, cmdOpts: PushOptions) => { + .action(async (journeys, cmdOpts: PushOptions) => { try { - const cliArgs = { inline: false }; - await loadTestFiles(cliArgs, files); + await loadTestFiles({ inline: false }, journeys); const options = normalizeOptions({ ...program.opts(), ...cmdOpts }); + if (!options.schedule) { + throw error(`Set default schedule in minutes for all monitors via '--schedule ' OR + configure via Synthetics config file under 'monitors.schedule' field.`); + } + + if (!options.locations) { + throw error(`Set default location for all monitors via CLI as '--locations ' OR + configure via Synthetics config file under 'monitors.locations' field.`); + } const monitors = runner.buildMonitors(options); await push(monitors, cmdOpts); } catch (e) { - console.error(e); + e && console.error(e); process.exit(1); } }); @@ -198,7 +212,7 @@ program const generator = await new Generator(resolve(process.cwd(), dir)); await generator.setup(); } catch (e) { - error(e); + e && error(e); process.exit(1); } }); diff --git a/src/generator/index.ts b/src/generator/index.ts index dabe8c49..b59cbece 100644 --- a/src/generator/index.ts +++ b/src/generator/index.ts @@ -27,13 +27,20 @@ import { existsSync } from 'fs'; import { mkdir, readFile, writeFile } from 'fs/promises'; import { bold, cyan, yellow } from 'kleur/colors'; import { join, relative, dirname, basename } from 'path'; +import { prompt } from 'enquirer'; +import { SyntheticsLocations } from '../dsl/monitor'; import { progress, write as stdWrite } from '../helpers'; -import { getPackageManager, runCommand } from './utils'; +import { getPackageManager, replaceTemplates, runCommand } from './utils'; // Templates that are required for setting up new // synthetics project const templateDir = join(__dirname, '..', '..', 'templates'); +type PromptOptions = { + locations: string; + schedule: number; +}; + export class Generator { pkgManager = 'npm'; constructor(public projectDir: string) {} @@ -44,13 +51,37 @@ export class Generator { } } - async files() { + async questions() { + if (process.env.TEST_QUESTIONS) { + return JSON.parse(process.env.TEST_QUESTIONS); + } + const question = [ + { + type: 'select', + name: 'locations', + message: 'Select the default location where you want to run monitors.', + choices: SyntheticsLocations, + }, + { + type: 'numeral', + name: 'schedule', + message: 'Set default schedule in minutes for all monitors', + initial: 10, + }, + ]; + return await prompt(question); + } + + async files(answers: PromptOptions) { const fileMap = new Map(); // Setup Synthetics config file const configFile = 'synthetics.config.ts'; fileMap.set( configFile, - await readFile(join(templateDir, configFile), 'utf-8') + replaceTemplates( + await readFile(join(templateDir, configFile), 'utf-8'), + answers + ) ); // Setup example journey file @@ -160,8 +191,9 @@ Visit https://www.elastic.co/guide/en/observability/master/synthetics-journeys.h async setup() { await this.directory(); + const answers = await this.questions(); await this.package(); - await this.files(); + await this.files(answers); await this.patchPkgJSON(); await this.patchGitIgnore(); this.banner(); diff --git a/src/generator/utils.ts b/src/generator/utils.ts index 5745dc6d..8bc148cd 100644 --- a/src/generator/utils.ts +++ b/src/generator/utils.ts @@ -39,3 +39,16 @@ export function runCommand(pkgManager: string, command: string) { } return `npm run ${command}`; } + +export function replaceTemplates(input: string, literals: Record) { + for (const key in literals) { + const finalValue = literals[key]; + input = input.replace(new RegExp(`'{{` + key + `}}'`, 'g'), () => { + if (typeof finalValue == 'number') { + return Number(finalValue); + } + return finalValue; + }); + } + return input; +} diff --git a/src/options.ts b/src/options.ts index 858fac0c..64916065 100644 --- a/src/options.ts +++ b/src/options.ts @@ -27,7 +27,6 @@ import merge from 'deepmerge'; import { readConfig } from './config'; import { getNetworkConditions, DEFAULT_THROTTLING_OPTIONS } from './helpers'; import type { CliArgs, RunOptions, ThrottlingOptions } from './common_types'; -import { MonitorConfig } from './dsl/monitor'; /** * Set debug based on DEBUG ENV and -d flags @@ -110,11 +109,14 @@ export function normalizeOptions(cliArgs: CliArgs): RunOptions { }, ]); - const defaults = getDefaultMonitorConfig(); + /** + * Get the default monitor config from synthetics.config.ts file + */ + const monitor = config.monitor; if (cliArgs.throttling) { const throttleConfig = merge.all([ - defaults.throttling, - config.monitor?.throttling || {}, + DEFAULT_THROTTLING_OPTIONS, + monitor?.throttling || {}, cliArgs.throttling as ThrottlingOptions, ]); options.throttling = throttleConfig; @@ -126,26 +128,12 @@ export function normalizeOptions(cliArgs: CliArgs): RunOptions { options.throttling = {}; } - options.locations = - cliArgs.locations ?? config.monitor?.locations ?? defaults.locations; - - options.schedule = - cliArgs.schedule ?? config.monitor?.schedule ?? defaults.schedule; + options.schedule = cliArgs.schedule ?? monitor?.schedule; + options.locations = cliArgs.locations ?? monitor?.locations; return options; } -/** - * Get the default monitor configuration for all journeys - */ -export function getDefaultMonitorConfig(): MonitorConfig { - return { - throttling: DEFAULT_THROTTLING_OPTIONS, - locations: ['us_east'], - schedule: 10, - }; -} - /** * Parses the throttling CLI settings and also * adapts to the format to keep the backwards compatability diff --git a/templates/synthetics.config.ts b/templates/synthetics.config.ts index 93109b17..7f0f385d 100644 --- a/templates/synthetics.config.ts +++ b/templates/synthetics.config.ts @@ -11,7 +11,10 @@ export default env => { /** * Configure global monitor settings */ - monitor: {}, + monitor: { + schedule: '{{schedule}}', + locations: ["'{{locations}}'"], + }, }; if (env !== 'development') { /**