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

[Synthetics] Template string syntax breaks inline monitors when running against the service #176094

Open
wants to merge 66 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
66 commits
Select commit Hold shift + click to select a range
be2a4e8
PoC stage of fix.
justinkambic Feb 1, 2024
9993d23
Include expect in list of imports.
justinkambic Feb 2, 2024
1bc597c
Add writable stream and zip for add/edit/sync inline monitors.
justinkambic Feb 9, 2024
643ab7a
Fix run_once.
justinkambic Feb 9, 2024
c1dadd4
Encapsulate.
justinkambic Feb 9, 2024
15cdea6
PR cleanup.
justinkambic Feb 12, 2024
45246c2
PR feedback.
justinkambic Feb 14, 2024
48255f4
PR feedback.
justinkambic Feb 14, 2024
9de233d
Simplify per PR advice.
justinkambic Feb 19, 2024
ce574a9
Improve tests, reorganize code.
justinkambic Feb 19, 2024
8a599ca
Merge branch 'main' into 169963/inline-scripts-with-template-syntax-f…
justinkambic Sep 18, 2024
b43ab54
Merge branch 'main' into 169963/inline-scripts-with-template-syntax-f…
justinkambic Sep 20, 2024
130d1d8
Merge branch 'main' into 169963/inline-scripts-with-template-syntax-f…
justinkambic Sep 23, 2024
4473510
Working to clean up tests. WIP.
justinkambic Sep 23, 2024
27b5060
Fix tests.
justinkambic Sep 24, 2024
6b9669f
Merge branch 'main' into 169963/inline-scripts-with-template-syntax-f…
justinkambic Sep 24, 2024
771b15a
Delete unneeded file.
justinkambic Sep 24, 2024
5d796af
Merge branch 'main' into 169963/inline-scripts-with-template-syntax-f…
justinkambic Sep 25, 2024
5cbe70e
Fix project field mapping issue where monitor type is not browser.
justinkambic Sep 25, 2024
f4bc77d
Default to whatever the inline script is if zipping should fail.
justinkambic Sep 25, 2024
c5ace2c
Fix types.
justinkambic Sep 25, 2024
a5eb787
Simplify.
justinkambic Sep 25, 2024
64d7bb8
Add test.
justinkambic Sep 26, 2024
c11b121
Touch up promise.
justinkambic Sep 26, 2024
1967d7d
Merge branch 'main' into 169963/inline-scripts-with-template-syntax-f…
justinkambic Sep 26, 2024
abecb34
Remove regression from monitor GET API.
justinkambic Sep 26, 2024
02c81ef
Modify utility API to use object param instead of individual args.
justinkambic Sep 26, 2024
bdfb632
Merge branch 'main' into 169963/inline-scripts-with-template-syntax-f…
justinkambic Sep 26, 2024
bc6be09
Fix types.
justinkambic Sep 30, 2024
b74a7af
Merge branch 'main' into 169963/inline-scripts-with-template-syntax-f…
justinkambic Sep 30, 2024
f996e4c
Merge branch 'main' into 169963/inline-scripts-with-template-syntax-f…
justinkambic Oct 1, 2024
f530675
Add a test.
justinkambic Oct 1, 2024
816ea89
Merge branch 'main' into 169963/inline-scripts-with-template-syntax-f…
justinkambic Oct 2, 2024
3aaf679
Fix run_once.
justinkambic Oct 2, 2024
ca237b2
Test run_once.
justinkambic Oct 2, 2024
841f17f
Unroll changes to `formatHeartbeatRequest`.
justinkambic Oct 2, 2024
caff1d0
Rename `AddEditMonitorAPI` to `UpsertMonitorAPI`.
justinkambic Oct 2, 2024
210387a
Fix all usages of `UpsertMonitorAPI`.
justinkambic Oct 2, 2024
5904329
Unbreak formatter tests.
justinkambic Oct 2, 2024
c93625f
Rename upsert api file.
justinkambic Oct 2, 2024
c96dde9
Rename test file.
justinkambic Oct 2, 2024
cc09e3f
Remove inline zip procedure from hydrate function.
justinkambic Oct 2, 2024
7c4af54
Make hydrate function sync again.
justinkambic Oct 2, 2024
743de32
Modify edit procedure to zip project and omit inline script field.
justinkambic Oct 2, 2024
1a7ff07
Rename a var.
justinkambic Oct 2, 2024
1dd2482
Remove unneeded async/await.
justinkambic Oct 2, 2024
0224832
Remove unneeded async/await.
justinkambic Oct 2, 2024
f8d8e8f
Simplify.
justinkambic Oct 2, 2024
ac78c00
Fix ping heatmap payload.
justinkambic Oct 4, 2024
b316cc9
Merge branch 'main' into 169963/inline-scripts-with-template-syntax-f…
justinkambic Oct 4, 2024
a86d955
Fix zip for persist.
justinkambic Oct 4, 2024
a4dee3b
Fix types.
justinkambic Oct 4, 2024
2df2554
Merge branch 'main' into 169963/inline-scripts-with-template-syntax-f…
justinkambic Oct 9, 2024
abbcd79
Merge branch 'main' of github.com:elastic/kibana into 169963/inline-s…
justinkambic Oct 14, 2024
0cc010c
Merge branch 'main' into 169963/inline-scripts-with-template-syntax-f…
justinkambic Oct 16, 2024
13c6907
Do not drop inline script when persisting a monitor edit.
justinkambic Oct 16, 2024
3e8bf39
Fix broken edit unit test.
justinkambic Oct 16, 2024
440015a
Simplify project mapping API.
justinkambic Oct 16, 2024
32bf01a
Include `mfa` keyword in script wrapper.
justinkambic Oct 16, 2024
823143d
Add fixed date to archiver call.
justinkambic Oct 16, 2024
09782ef
Fix types.
justinkambic Oct 16, 2024
6e12216
Fix broken unit tests.
justinkambic Oct 16, 2024
716c2b2
TEMP
justinkambic Oct 30, 2024
7d6ae2b
Merge branch 'main' into 169963/inline-scripts-with-template-syntax-f…
justinkambic Oct 30, 2024
53f5ff1
Fix delete crud api.
justinkambic Oct 30, 2024
84294c7
Revert "TEMP"
justinkambic Oct 30, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,8 @@ export const BrowserSensitiveSimpleFieldsCodec = t.intersection([
CommonFieldsCodec,
]);

export type BrowserSensitiveSimpleFields = t.TypeOf<typeof BrowserSensitiveSimpleFieldsCodec>;

export const ThrottlingConfigValueCodec = t.interface({
download: t.string,
upload: t.string,
Expand Down
54 changes: 54 additions & 0 deletions x-pack/plugins/synthetics/server/common/mem_writable.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import { inlineToProjectZip, MemWritable } from './mem_writable';

describe('MemWritable', () => {
it('should write chunks to the buffer', async () => {
const memWritable = new MemWritable();

const chunk1 = `step('goto', () => page.goto('https://elastic.co'));`;
const chunk2 = `step('throw error', () => { throw new Error('error'); });`;
const expectedBuffer = Buffer.from(chunk1 + chunk2);

await new Promise<void>((resolve, reject) => {
memWritable.write(Buffer.from(chunk1), (err) => {
if (err) {
reject(err);
}
expect(memWritable.buffer).toEqual(Buffer.from(chunk1));
resolve();
});
});
await new Promise<void>((resolve, reject) => {
memWritable.write(Buffer.from(chunk2), (err) => {
if (err) {
reject(err);
}
resolve();
});
});
expect(memWritable.buffer).toEqual(expectedBuffer);
});
});

describe('inlineToProjectZip', () => {
it('should return base64 encoded zip data', async () => {
const inlineJourney = `
step('goto', () => page.goto('https://elastic.co'));
step('throw error', () => { throw new Error('error'); });
`;
const monitorId = 'testMonitorId';
const logger = jest.fn();

// @ts-expect-error not checking logger functionality
const result = await inlineToProjectZip(inlineJourney, monitorId, logger);

// zip is not deterministic, so we can't check the exact value
expect(result.length).toBeGreaterThan(0);
});
});
70 changes: 70 additions & 0 deletions x-pack/plugins/synthetics/server/common/mem_writable.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import { Logger } from '@kbn/logging';
import archiver from 'archiver';
import { Writable, WritableOptions } from 'node:stream';

export class MemWritable extends Writable {
private _buffer: Buffer;
constructor(opts?: WritableOptions) {
super(opts);
this._buffer = Buffer.from('');
}

public get buffer(): Buffer {
return this._buffer;
}

_write(
justinkambic marked this conversation as resolved.
Show resolved Hide resolved
chunk: any,
_encoding: BufferEncoding,
callback: (error?: Error | null | undefined) => void
): void {
try {
this._buffer = Buffer.concat([this._buffer, chunk]);
callback();
} catch (e) {
callback(e);
}
}
}

function wrapInlineInProject(inlineJourney: string) {
return `import { journey, step, expect } from '@elastic/synthetics';

journey('inline', ({ page, context, browser, params, request }) => {
${inlineJourney}
});
`;
}

export async function inlineToProjectZip(
inlineJourney: string,
monitorId: string,
logger: Logger
): Promise<string> {
const mWriter = new MemWritable();
try {
await new Promise((resolve, reject) => {
const archive = archiver('zip', {
zlib: { level: 9 },
});
archive.on('error', reject);
mWriter.on('close', resolve);
archive.pipe(mWriter);
archive.append(wrapInlineInProject(inlineJourney), {
name: 'inline.journey.ts',
});
archive.finalize();
});
} catch (e) {
logger.error(`Failed to create zip for inline monitor ${monitorId}`);
justinkambic marked this conversation as resolved.
Show resolved Hide resolved
throw e;
}
return mWriter.buffer.toString('base64');
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import { ConfigKey, SyntheticsMonitor } from '../../../common/runtime_types';
import { hydrateMonitorFields } from './add_monitor';

describe('hydrateMonitorFields', () => {
it('creates expected zip b64 project field value for inline browser monitor', async () => {
const normalizedMonitor: SyntheticsMonitor = {
// @ts-expect-error extra field
type: 'browser',
// @ts-expect-error extra field
form_monitor_type: 'multistep',
enabled: true,
alert: { status: { enabled: true }, tls: { enabled: true } },
// @ts-expect-error extra field
schedule: { unit: 'm', number: '10' },
'service.name': '',
config_id: '',
tags: [],
timeout: null,
name: 'test-once-more',
locations: [{ id: 'dev', label: 'Dev Service', isServiceManaged: true }],
namespace: 'default',
// @ts-expect-error extra field
origin: 'ui',
journey_id: '',
hash: '',
id: '',
params: '',
max_attempts: 2,
project_id: '',
playwright_options: '',
__ui: { script_source: { is_generated_script: false, file_name: '' } },
'url.port': null,
'source.inline.script': `step('goto', () => page.goto('https://elastic.co'))
step('fail', () => {
throw Error('fail');
})`,
'source.project.content': '',
playwright_text_assertion: '',
urls: '',
screenshots: 'on',
synthetics_args: [],
'filter_journeys.match': '',
'filter_journeys.tags': [],
ignore_https_errors: false,
throttling: {
value: { download: '5', upload: '3', latency: '20' },
id: 'default',
label: 'Default',
},
'ssl.certificate_authorities': '',
'ssl.certificate': '',
'ssl.key': '',
'ssl.key_passphrase': '',
'ssl.verification_mode': 'full',
'ssl.supported_protocols': ['TLSv1.1', 'TLSv1.2', 'TLSv1.3'],
};

const hydratedMonitor = await hydrateMonitorFields({
normalizedMonitor,
newMonitorId: 'testMonitorId',
routeContext: {
// @ts-expect-error not checking request functionality
request: {
query: {
preserve_namespace: true,
},
},
server: {
// @ts-expect-error not checking logger functionality
logger: jest.fn(),
},
},
});

expect(hydratedMonitor[ConfigKey.SOURCE_PROJECT_CONTENT]).toBeDefined();
// zip is not deterministic, so we can't check the exact value
expect(hydratedMonitor[ConfigKey.SOURCE_PROJECT_CONTENT]).length.toBeGreaterThan(0);
});

it('does not add b64 zip data to lightweight monitors', async () => {
const newMonitorId = 'testMonitorId';
const routeContext = {
request: {
query: {
preserve_namespace: true,
},
},
server: {
logger: jest.fn(),
},
};
const normalizedMonitor: SyntheticsMonitor = {
// @ts-expect-error extra field
type: 'tcp',
// @ts-expect-error extra field
form_monitor_type: 'tcp',
enabled: true,
alert: { status: { enabled: true }, tls: { enabled: true } },
// @ts-expect-error extra field
schedule: { number: '3', unit: 'm' },
'service.name': '',
config_id: '',
tags: [],
timeout: '16',
name: 'tcp://google.com:80',
locations: [{ id: 'dev', label: 'Dev Service', isServiceManaged: true }],
namespace: 'default',
// @ts-expect-error extra field
origin: 'ui',
journey_id: '',
hash: '',
id: '',
params: '',
max_attempts: 2,
__ui: { is_tls_enabled: false },
hosts: 'tcp://google.com:80',
urls: '',
'url.port': null,
proxy_url: '',
proxy_use_local_resolver: false,
'check.receive': '',
'check.send': '',
mode: 'any',
ipv4: true,
ipv6: true,
'ssl.certificate_authorities': '',
'ssl.certificate': '',
'ssl.key': '',
'ssl.key_passphrase': '',
'ssl.verification_mode': 'full',
'ssl.supported_protocols': ['TLSv1.1', 'TLSv1.2', 'TLSv1.3'],
};
const hydratedMonitor = await hydrateMonitorFields({
normalizedMonitor,
newMonitorId,
// @ts-expect-error not checking routeContext functionality
routeContext,
});

expect(hydratedMonitor[ConfigKey.SOURCE_PROJECT_CONTENT]).toEqual('');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
MonitorFields,
SyntheticsMonitor,
EncryptedSyntheticsMonitorAttributes,
BrowserSensitiveSimpleFields,
} from '../../../common/runtime_types';
import { formatKibanaNamespace } from '../../synthetics_service/formatters/private_formatters';
import { getPrivateLocations } from '../../synthetics_service/get_private_locations';
Expand All @@ -38,6 +39,7 @@ import { sendTelemetryEvents, formatTelemetryEvent } from '../telemetry/monitor_
import { formatSecrets } from '../../synthetics_service/utils/secrets';
import { deleteMonitor } from './delete_monitor';
import { mapSavedObjectToMonitor } from './helper';
import { inlineToProjectZip } from '../../common/mem_writable';

export const addSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () => ({
method: 'POST',
Expand Down Expand Up @@ -137,7 +139,7 @@ export const createNewSavedObjectMonitor = async ({
);
};

export const hydrateMonitorFields = ({
export const hydrateMonitorFields = async ({
newMonitorId,
normalizedMonitor,
routeContext,
Expand All @@ -152,13 +154,21 @@ export const hydrateMonitorFields = ({
string,
{ preserve_namespace?: boolean }
>;

const inlineSource = (normalizedMonitor as BrowserSensitiveSimpleFields)?.[
ConfigKey.SOURCE_INLINE
] as string | undefined;

return {
...normalizedMonitor,
[ConfigKey.MONITOR_QUERY_ID]: normalizedMonitor[ConfigKey.CUSTOM_HEARTBEAT_ID] || newMonitorId,
[ConfigKey.CONFIG_ID]: newMonitorId,
[ConfigKey.NAMESPACE]: preserveNamespace
? normalizedMonitor[ConfigKey.NAMESPACE]
: getMonitorNamespace(server, request, normalizedMonitor[ConfigKey.NAMESPACE]),
[ConfigKey.SOURCE_PROJECT_CONTENT]: !!inlineSource
? await inlineToProjectZip(inlineSource, newMonitorId, server.logger)
: '',
};
};

Expand All @@ -177,7 +187,7 @@ export const syncNewMonitor = async ({
const newMonitorId = id ?? uuidV4();

let monitorSavedObject: SavedObject<EncryptedSyntheticsMonitorAttributes> | null = null;
const monitorWithNamespace = hydrateMonitorFields({
const monitorWithNamespace = await hydrateMonitorFields({
normalizedMonitor,
routeContext,
newMonitorId,
Expand Down
Loading