Skip to content

Commit

Permalink
feat: interactive scaffolding for picking default monitor config (#508)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>

Co-authored-by: Andrew Cholakian <[email protected]>
  • Loading branch information
vigneshshanmugam and andrewvc authored Jun 3, 2022
1 parent c24a991 commit a4c10b5
Show file tree
Hide file tree
Showing 12 changed files with 181 additions and 52 deletions.
14 changes: 8 additions & 6 deletions __tests__/core/runner.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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({
Expand All @@ -700,15 +703,14 @@ describe('runner', () => {
throttling: { download: 5, latency: 20, upload: 3 },
});
expect(monitors[1].config).toMatchObject({
locations: ['us_east'],
schedule: 10,
throttling: { latency: 1000 },
});
});

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 },
Expand All @@ -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({
Expand Down
17 changes: 16 additions & 1 deletion __tests__/generator/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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();
Expand Down
3 changes: 0 additions & 3 deletions __tests__/options.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
67 changes: 67 additions & 0 deletions __tests__/push/index.test.ts
Original file line number Diff line number Diff line change
@@ -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`);
});
});
4 changes: 2 additions & 2 deletions __tests__/utils/test-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ export class CLIMock {
private waitForPromise: () => void;
private cliArgs: string[] = [];
private stdinStr?: string;
private stderrStr: string;
private stderrStr = '';
exitCode: Promise<number>;

constructor(public debug: boolean = false) {}
Expand All @@ -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],
Expand Down
7 changes: 2 additions & 5 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
34 changes: 24 additions & 10 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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 <time-in-minutes>',
Expand All @@ -162,8 +168,8 @@ program
).choices(SyntheticsLocations)
)
.requiredOption(
'--project <id/name>',
'project/repository that will be used for grouping the monitors.'
'--project <project-id>',
'id that will be used for logically grouping monitors'
)
.requiredOption('--url <url>', 'kibana URL to upload the monitors')
.requiredOption(
Expand All @@ -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 <time-in-minutes>' 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 <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);
}
});
Expand All @@ -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);
}
});
Expand Down
40 changes: 36 additions & 4 deletions src/generator/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {}
Expand All @@ -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<PromptOptions>(question);
}

async files(answers: PromptOptions) {
const fileMap = new Map<string, string>();
// 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
Expand Down Expand Up @@ -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();
Expand Down
13 changes: 13 additions & 0 deletions src/generator/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,16 @@ export function runCommand(pkgManager: string, command: string) {
}
return `npm run ${command}`;
}

export function replaceTemplates(input: string, literals: Record<string, any>) {
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;
}
Loading

0 comments on commit a4c10b5

Please sign in to comment.