Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: delete entire project via push command #598

Merged
merged 2 commits into from Sep 19, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 0 additions & 5 deletions __tests__/push/__snapshots__/index.test.ts.snap
Expand Up @@ -18,11 +18,6 @@ exports[`Push API handle sync response 1`] = `
"
`;

exports[`Push abort on push with different project id 1`] = `
"⚠ Push command Aborted
"
`;

exports[`Push error on empty project id 1`] = `
"Aborted. Invalid synthetics project settings.

Expand Down
35 changes: 32 additions & 3 deletions __tests__/push/index.test.ts
Expand Up @@ -99,18 +99,25 @@ describe('Push', () => {
const output = await runPush([...DEFAULT_ARGS, '--id', 'new-project'], {
TEST_OVERRIDE: '',
});
expect(output).toMatchSnapshot();
expect(output).toContain('Push command Aborted');
});

it('push with different id when overriden', async () => {
await fakeProjectSetup(
{ id: 'test-project' },
{ id: 'test-project', space: 'dummy', url: 'http://localhost:8080' },
{ locations: ['test-loc'], schedule: 2 }
);
const testJourney = join(PROJECT_DIR, 'test.journey.ts');
await writeFile(
testJourney,
`import {journey, monitor} from '../../../src/index';
journey('journey 1', () => monitor.use({ id: 'j1' }));`
);
const output = await runPush([...DEFAULT_ARGS, '--id', 'new-project'], {
TEST_OVERRIDE: 'true',
});
expect(output).toContain('No Monitors found');
expect(output).toContain('preparing all monitors');
await rm(testJourney, { force: true });
});

it('errors on duplicate monitors', async () => {
Expand Down Expand Up @@ -150,6 +157,28 @@ journey('duplicate name', () => monitor.use({ schedule: 20 }));`
expect(formatDuplicateError(duplicates as Set<Monitor>)).toMatchSnapshot();
});

it('abort when delete is skipped', async () => {
await fakeProjectSetup(
{ id: 'test-project' },
{ locations: ['test-loc'], schedule: 2 }
);
const output = await runPush([...DEFAULT_ARGS], {
TEST_OVERRIDE: '',
});
expect(output).toContain('Push command Aborted');
});

it('delete entire project ', async () => {
await fakeProjectSetup(
{ id: 'test-project', space: 'dummy', url: 'http://localhost:8080' },
{ locations: ['test-loc'], schedule: 2 }
);
const output = await runPush([...DEFAULT_ARGS], {
TEST_OVERRIDE: 'true',
});
expect(output).toContain('deleting all stale monitors');
});

describe('API', () => {
let server: Server;
beforeAll(async () => {
Expand Down
114 changes: 64 additions & 50 deletions src/push/index.ts
Expand Up @@ -25,7 +25,7 @@

import { readFile, writeFile } from 'fs/promises';
import { prompt } from 'enquirer';
import { bold, yellow } from 'kleur/colors';
import { bold } from 'kleur/colors';
import {
ok,
formatAPIError,
Expand All @@ -49,24 +49,42 @@ import type { PushOptions, ProjectSettings } from '../common_types';
import { findSyntheticsConfig, readConfig } from '../config';

export async function push(monitors: Monitor[], options: PushOptions) {
if (monitors.length === 0) {
throw 'No Monitors found';
}
const duplicates = trackDuplicates(monitors);
if (duplicates.size > 0) {
throw error(formatDuplicateError(duplicates));
}
let schemas: MonitorSchema[] = [];
if (monitors.length > 0) {
const duplicates = trackDuplicates(monitors);
if (duplicates.size > 0) {
throw error(formatDuplicateError(duplicates));
}

progress(`preparing all monitors`);
const schemas = await buildMonitorSchema(monitors);
progress(`preparing all monitors`);
schemas = await buildMonitorSchema(monitors);

progress(`creating all monitors`);
for (const schema of schemas) {
await pushMonitors({ schemas: [schema], keepStale: true, options });
}
progress(`creating all monitors`);
for (const schema of schemas) {
await pushMonitors({ schemas: [schema], keepStale: true, options });
}
} else {
write('');
// Makes testing easier with overrides
let deleteAll = false;
if (process.env.TEST_OVERRIDE != null) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should add a --yes flag to skip this in case someone wants to automate it for any reasons.

I did try running with yes | npx @elastic/synthetics push but our command really dislikes that and tries to process all the input thus stalling.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will address this separately and open a PR. #601

deleteAll = Boolean(process.env.TEST_OVERRIDE);
} else {
({ deleteAll } = await prompt<{ deleteAll: boolean }>({
type: 'confirm',
name: 'deleteAll',
message: `Pushing without any monitors will delete all monitors associated with the project.\n Do you want to continue?`,
initial: false,
}));
}

if (!deleteAll) {
throw aborted('Push command Aborted');
}
}
progress(`deleting all stale monitors`);
await pushMonitors({ schemas, keepStale: false, options });

done('Pushed');
}

Expand All @@ -79,43 +97,39 @@ export async function pushMonitors({
keepStale: boolean;
options: PushOptions;
}) {
try {
const { body, statusCode } = await createMonitors(
schemas,
options,
keepStale
);
if (statusCode === 404) {
throw formatNotFoundError(await body.text());
}
if (!ok(statusCode)) {
const { error, message } = await body.json();
throw formatAPIError(statusCode, error, message);
}
body.setEncoding('utf-8');
for await (const data of body) {
// Its kind of hacky for now where Kibana streams the response by
// writing the data as NDJSON events (data can be interleaved), we
// distinguish the final data by checking if the event was a progress vs complete event
const chunks = safeNDJSONParse(data);
for (const chunk of chunks) {
if (typeof chunk === 'string') {
// TODO: add progress back for all states once we get the fix
// on kibana side
keepStale && apiProgress(chunk);
continue;
}
const { failedMonitors, failedStaleMonitors } = chunk;
if (failedMonitors && failedMonitors.length > 0) {
throw formatFailedMonitors(failedMonitors);
}
if (failedStaleMonitors.length > 0) {
write(yellow(formatStaleMonitors(failedStaleMonitors)));
}
const { body, statusCode } = await createMonitors(
schemas,
options,
keepStale
);
if (statusCode === 404) {
throw formatNotFoundError(await body.text());
}
if (!ok(statusCode)) {
const { error, message } = await body.json();
throw formatAPIError(statusCode, error, message);
}
body.setEncoding('utf-8');
for await (const data of body) {
// Its kind of hacky for now where Kibana streams the response by
// writing the data as NDJSON events (data can be interleaved), we
// distinguish the final data by checking if the event was a progress vs complete event
const chunks = safeNDJSONParse(data);
for (const chunk of chunks) {
if (typeof chunk === 'string') {
// TODO: add progress back for all states once we get the fix
// on kibana side
keepStale && apiProgress(chunk);
continue;
}
const { failedMonitors, failedStaleMonitors } = chunk;
if (failedMonitors && failedMonitors.length > 0) {
throw formatFailedMonitors(failedMonitors);
}
if (failedStaleMonitors.length > 0) {
throw formatStaleMonitors(failedStaleMonitors);
}
}
} catch (e) {
error(e);
}
}

Expand Down Expand Up @@ -149,7 +163,7 @@ const INSTALLATION_HELP = `Run 'npx @elastic/synthetics init' to create project

export async function loadSettings() {
try {
const config = await readConfig('asd');
const config = await readConfig(process.env['NODE_ENV'] || 'development');
// Missing config file, fake throw to capture as missing file
if (Object.keys(config).length === 0) {
throw '';
Expand Down
16 changes: 9 additions & 7 deletions src/push/request.ts
Expand Up @@ -23,7 +23,7 @@
*
*/

import { bold } from 'kleur/colors';
import { bold, red, yellow } from 'kleur/colors';
import { Dispatcher, request } from 'undici';
import { indent, symbols } from '../helpers';

Expand All @@ -47,7 +47,7 @@ export async function sendRequest(options: APIRequestOptions) {
'user-agent': `Elastic/Synthetics ${version}`,
'kbn-xsrf': 'true',
},
headersTimeout: 60 * 1000
headersTimeout: 60 * 1000,
});
}

Expand All @@ -62,8 +62,10 @@ export type APIMonitorError = {
};

export function formatNotFoundError(message: string) {
return bold(
`${symbols['failed']} Please check your kibana url and try again - 404:${message}`
return red(
bold(
`${symbols['failed']} Please check your kibana url and try again - 404:${message}`
)
);
}

Expand All @@ -78,7 +80,7 @@ export function formatAPIError(
);
inner += indent(message, ' ');
outer += indent(inner);
return outer;
return red(outer);
}

function formatMonitorError(errors: APIMonitorError[]) {
Expand All @@ -95,10 +97,10 @@ function formatMonitorError(errors: APIMonitorError[]) {

export function formatFailedMonitors(errors: APIMonitorError[]) {
const heading = bold(`${symbols['failed']} Error\n`);
return heading + formatMonitorError(errors);
return red(heading + formatMonitorError(errors));
}

export function formatStaleMonitors(errors: APIMonitorError[]) {
const heading = bold(`${symbols['warning']} Warnings\n`);
return heading + formatMonitorError(errors);
return yellow(heading + formatMonitorError(errors));
}