[Synthetics] Project monitors - support lightweight project monitors (#141066)

* Improve project formatter

* update

* update imports

* adjust logic and add tests for lightweight project monitors

* update test

* update api test

* test

* update more test

* more fix

* update tests

* [CI] Auto-commit changed files from 'node scripts/precommit_hook.js --ref HEAD~1..HEAD --fix'

* remove references to preserve monitor id

* [CI] Auto-commit changed files from 'node scripts/eslint --no-cache --fix'

* adjust errors

* adjust tests

* [CI] Auto-commit changed files from 'node scripts/eslint --no-cache --fix'

* update

* code reuse

* update type

* add tests

* update

* update

* fix types

* deepclone policies

* update test

* add tests

* update type

* Update src/plugins/interactive_setup/public/theme/kibana_theme_provider.tsx

* [CI] Auto-commit changed files from 'node scripts/precommit_hook.js --ref HEAD~1..HEAD --fix'

Co-authored-by: shahzad31 <shahzad.muhammad@elastic.co>
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Dominique Clarke 2022-09-20 17:19:21 -04:00 committed by GitHub
parent e82b7ff582
commit fa636c655b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
36 changed files with 1632 additions and 551 deletions

View file

@ -6,7 +6,8 @@
* Side Public License, v 1.
*/
import { EuiProvider, EuiProviderProps } from '@elastic/eui';
import type { EuiProviderProps } from '@elastic/eui';
import { EuiProvider } from '@elastic/eui';
import createCache from '@emotion/cache';
import type { FC } from 'react';
import React, { useMemo } from 'react';

View file

@ -46,6 +46,7 @@ export const DEFAULT_COMMON_FIELDS: CommonFields = {
[ConfigKey.LOCATIONS]: [],
[ConfigKey.NAMESPACE]: DEFAULT_NAMESPACE_STRING,
[ConfigKey.MONITOR_SOURCE_TYPE]: SourceType.UI,
[ConfigKey.JOURNEY_ID]: '',
};
export const DEFAULT_BROWSER_ADVANCED_FIELDS: BrowserAdvancedFields = {
@ -63,7 +64,6 @@ export const DEFAULT_BROWSER_ADVANCED_FIELDS: BrowserAdvancedFields = {
export const DEFAULT_BROWSER_SIMPLE_FIELDS: BrowserSimpleFields = {
...DEFAULT_COMMON_FIELDS,
[ConfigKey.JOURNEY_ID]: '',
[ConfigKey.PROJECT_ID]: '',
[ConfigKey.PLAYWRIGHT_OPTIONS]: '',
[ConfigKey.METADATA]: {
@ -96,6 +96,7 @@ export const DEFAULT_BROWSER_SIMPLE_FIELDS: BrowserSimpleFields = {
[ConfigKey.ZIP_URL_TLS_VERSION]: undefined,
[ConfigKey.URLS]: '',
[ConfigKey.FORM_MONITOR_TYPE]: FormMonitorType.MULTISTEP,
[ConfigKey.TIMEOUT]: null,
};
export const DEFAULT_HTTP_SIMPLE_FIELDS: HTTPSimpleFields = {

View file

@ -72,7 +72,6 @@ export const browserFormatters: BrowserFormatMap = {
arrayToJsonFormatter(fields[ConfigKey.JOURNEY_FILTERS_TAGS]),
[ConfigKey.THROTTLING_CONFIG]: throttlingFormatter,
[ConfigKey.IGNORE_HTTPS_ERRORS]: null,
[ConfigKey.JOURNEY_ID]: null,
[ConfigKey.PROJECT_ID]: null,
[ConfigKey.PLAYWRIGHT_OPTIONS]: null,
[ConfigKey.CUSTOM_HEARTBEAT_ID]: null,

View file

@ -28,6 +28,7 @@ export const commonFormatters: CommonFormatMap = {
[ConfigKey.REVISION]: null,
[ConfigKey.MONITOR_SOURCE_TYPE]: null,
[ConfigKey.FORM_MONITOR_TYPE]: null,
[ConfigKey.JOURNEY_ID]: null,
};
export const arrayToJsonFormatter = (value: string[] = []) =>

View file

@ -37,8 +37,8 @@ export const BandwidthLimitKeyCodec = tEnum<BandwidthLimitKey>(
export type BandwidthLimitKeyType = t.TypeOf<typeof BandwidthLimitKeyCodec>;
export const LocationGeoCodec = t.interface({
lat: t.number,
lon: t.number,
lat: t.union([t.string, t.number]),
lon: t.union([t.string, t.number]),
});
export const LocationStatusCodec = tEnum<LocationStatus>('LocationStatus', LocationStatus);
@ -77,13 +77,13 @@ export const PublicLocationsCodec = t.array(PublicLocationCodec);
export const MonitorServiceLocationCodec = t.intersection([
t.interface({
id: t.string,
isServiceManaged: t.boolean,
}),
t.partial({
label: t.string,
geo: LocationGeoCodec,
url: t.string,
isInvalid: t.boolean,
isServiceManaged: t.boolean,
}),
]);

View file

@ -8,11 +8,7 @@
import * as t from 'io-ts';
import { secretKeys } from '../../constants/monitor_management';
import { ConfigKey } from './config_key';
import {
MonitorServiceLocationsCodec,
MonitorServiceLocationCodec,
ServiceLocationErrors,
} from './locations';
import { MonitorServiceLocationCodec, ServiceLocationErrors } from './locations';
import {
DataStream,
DataStreamCodec,
@ -25,6 +21,7 @@ import {
VerificationModeCodec,
} from './monitor_configs';
import { MetadataCodec } from './monitor_meta_data';
import { PrivateLocationCodec } from './synthetics_private_locations';
const ScheduleCodec = t.interface({
number: t.string,
@ -77,7 +74,7 @@ export const CommonFieldsCodec = t.intersection([
[ConfigKey.SCHEDULE]: ScheduleCodec,
[ConfigKey.APM_SERVICE_NAME]: t.string,
[ConfigKey.TAGS]: t.array(t.string),
[ConfigKey.LOCATIONS]: MonitorServiceLocationsCodec,
[ConfigKey.LOCATIONS]: t.array(t.union([MonitorServiceLocationCodec, PrivateLocationCodec])),
}),
t.partial({
[ConfigKey.FORM_MONITOR_TYPE]: FormMonitorTypeCodec,
@ -85,6 +82,7 @@ export const CommonFieldsCodec = t.intersection([
[ConfigKey.REVISION]: t.number,
[ConfigKey.MONITOR_SOURCE_TYPE]: SourceTypeCodec,
[ConfigKey.CONFIG_ID]: t.string,
[ConfigKey.JOURNEY_ID]: t.string,
}),
]);
@ -218,7 +216,6 @@ export const EncryptedBrowserSimpleFieldsCodec = t.intersection([
}),
t.partial({
[ConfigKey.PLAYWRIGHT_OPTIONS]: t.string,
[ConfigKey.JOURNEY_ID]: t.string,
[ConfigKey.PROJECT_ID]: t.string,
[ConfigKey.ORIGINAL_SPACE]: t.string,
[ConfigKey.CUSTOM_HEARTBEAT_ID]: t.string,

View file

@ -14,19 +14,21 @@ export const ProjectMonitorThrottlingConfigCodec = t.interface({
latency: t.number,
});
export const ProjectBrowserMonitorCodec = t.intersection([
export const ProjectMonitorCodec = t.intersection([
t.interface({
type: t.string,
id: t.string,
name: t.string,
schedule: t.number,
content: t.string,
locations: t.array(t.string),
}),
t.partial({
content: t.string,
timeout: t.string,
privateLocations: t.array(t.string),
throttling: ProjectMonitorThrottlingConfigCodec,
screenshot: ScreenshotOptionCodec,
tags: t.array(t.string),
tags: t.union([t.string, t.array(t.string)]),
ignoreHTTPSErrors: t.boolean,
apmServiceName: t.string,
playwrightOptions: t.record(t.string, t.unknown),
@ -35,17 +37,21 @@ export const ProjectBrowserMonitorCodec = t.intersection([
}),
params: t.record(t.string, t.unknown),
enabled: t.boolean,
urls: t.union([t.string, t.array(t.string)]),
hosts: t.union([t.string, t.array(t.string)]),
max_redirects: t.string,
wait: t.string,
}),
]);
export const ProjectMonitorsRequestCodec = t.interface({
project: t.string,
keep_stale: t.boolean,
monitors: t.array(ProjectBrowserMonitorCodec),
monitors: t.array(ProjectMonitorCodec),
});
export type ProjectMonitorThrottlingConfig = t.TypeOf<typeof ProjectMonitorThrottlingConfigCodec>;
export type ProjectBrowserMonitor = t.TypeOf<typeof ProjectBrowserMonitorCodec>;
export type ProjectMonitor = t.TypeOf<typeof ProjectMonitorCodec>;
export type ProjectMonitorsRequest = t.TypeOf<typeof ProjectMonitorsRequestCodec>;

View file

@ -7,18 +7,23 @@
import * as t from 'io-ts';
export const PrivateLocationType = t.intersection([
export const PrivateLocationCodec = t.intersection([
t.interface({
label: t.string,
id: t.string,
agentPolicyId: t.string,
concurrentMonitors: t.number,
}),
t.partial({ geo: t.interface({ lat: t.number, lon: t.number }) }),
t.partial({
isServiceManaged: t.boolean,
/* Empty Lat lon was accidentally saved as an empty string instead of undefined or null
* Need a migration to fix */
geo: t.interface({ lat: t.union([t.string, t.number]), lon: t.union([t.string, t.number]) }),
}),
]);
export const SyntheticsPrivateLocationsType = t.type({
locations: t.array(PrivateLocationType),
locations: t.array(PrivateLocationCodec),
});
export type PrivateLocation = t.TypeOf<typeof PrivateLocationType>;
export type PrivateLocation = t.TypeOf<typeof PrivateLocationCodec>;
export type SyntheticsPrivateLocations = t.TypeOf<typeof SyntheticsPrivateLocationsType>;

View file

@ -104,6 +104,7 @@ describe('format', () => {
'check.response.status': [],
enabled,
form_monitor_type: 'http',
journey_id: '',
locations: [
{
id: 'us_central',
@ -316,6 +317,7 @@ describe('format', () => {
'check.response.status': [],
enabled: true,
form_monitor_type: 'http',
journey_id: '',
locations: [
{
id: 'us_central',

View file

@ -107,7 +107,6 @@ export const browserNormalizers: BrowserNormalizerMap = {
ConfigKey.JOURNEY_FILTERS_TAGS
),
[ConfigKey.IGNORE_HTTPS_ERRORS]: getBrowserNormalizer(ConfigKey.IGNORE_HTTPS_ERRORS),
[ConfigKey.JOURNEY_ID]: getBrowserNormalizer(ConfigKey.JOURNEY_ID),
[ConfigKey.PROJECT_ID]: getBrowserNormalizer(ConfigKey.PROJECT_ID),
[ConfigKey.PLAYWRIGHT_OPTIONS]: getBrowserNormalizer(ConfigKey.PLAYWRIGHT_OPTIONS),
[ConfigKey.CUSTOM_HEARTBEAT_ID]: getBrowserNormalizer(ConfigKey.CUSTOM_HEARTBEAT_ID),

View file

@ -92,4 +92,5 @@ export const commonNormalizers: CommonNormalizerMap = {
[ConfigKey.REVISION]: getCommonNormalizer(ConfigKey.REVISION),
[ConfigKey.MONITOR_SOURCE_TYPE]: getCommonNormalizer(ConfigKey.MONITOR_SOURCE_TYPE),
[ConfigKey.FORM_MONITOR_TYPE]: getCommonNormalizer(ConfigKey.FORM_MONITOR_TYPE),
[ConfigKey.JOURNEY_ID]: getCommonNormalizer(ConfigKey.JOURNEY_ID),
};

View file

@ -6,11 +6,12 @@
*/
import { schema } from '@kbn/config-schema';
import { UMServerLibs } from '../../legacy_uptime/lib/lib';
import { ProjectBrowserMonitor } from '../../../common/runtime_types';
import { ProjectMonitor } from '../../../common/runtime_types';
import { SyntheticsStreamingRouteFactory } from '../../legacy_uptime/routes/types';
import { API_URLS } from '../../../common/constants';
import { getAllLocations } from '../../synthetics_service/get_all_locations';
import { ProjectMonitorFormatter } from '../../synthetics_service/project_monitor_formatter';
import { ProjectMonitorFormatter } from '../../synthetics_service/project_monitor/project_monitor_formatter';
export const addSyntheticsProjectMonitorRoute: SyntheticsStreamingRouteFactory = (
libs: UMServerLibs
@ -32,7 +33,7 @@ export const addSyntheticsProjectMonitorRoute: SyntheticsStreamingRouteFactory =
subject,
}): Promise<any> => {
try {
const monitors = (request.body?.monitors as ProjectBrowserMonitor[]) || [];
const monitors = (request.body?.monitors as ProjectMonitor[]) || [];
const spaceId = server.spaces.spacesService.getSpaceId(request);
const { keep_stale: keepStale, project: projectId } = request.body || {};
const { publicLocations, privateLocations } = await getAllLocations(

View file

@ -10,8 +10,8 @@ import { formatErrors } from '@kbn/securitysolution-io-ts-utils';
import {
BrowserFieldsCodec,
ProjectBrowserMonitorCodec,
ProjectBrowserMonitor,
ProjectMonitorCodec,
ProjectMonitor,
ConfigKey,
DataStream,
DataStreamCodec,
@ -34,16 +34,18 @@ const monitorTypeToCodecMap: Record<DataStream, MonitorCodecType> = {
[DataStream.BROWSER]: BrowserFieldsCodec,
};
/**
* Validates monitor fields with respect to the relevant Codec identified by object's 'type' property.
* @param monitorFields {MonitorFields} The mixed type representing the possible monitor payload from UI.
*/
export function validateMonitor(monitorFields: MonitorFields): {
export interface ValidationResult {
valid: boolean;
reason: string;
details: string;
payload: object;
} {
}
/**
* Validates monitor fields with respect to the relevant Codec identified by object's 'type' property.
* @param monitorFields {MonitorFields} The mixed type representing the possible monitor payload from UI.
*/
export function validateMonitor(monitorFields: MonitorFields): ValidationResult {
const { [ConfigKey.MONITOR_TYPE]: monitorType } = monitorFields;
const decodedType = DataStreamCodec.decode(monitorType);
@ -82,15 +84,7 @@ export function validateMonitor(monitorFields: MonitorFields): {
return { valid: true, reason: '', details: '', payload: monitorFields };
}
export function validateProjectMonitor(
monitorFields: ProjectBrowserMonitor,
projectId: string
): {
valid: boolean;
reason: string;
details: string;
payload: object;
} {
export function validateProjectMonitor(monitorFields: ProjectMonitor): ValidationResult {
const locationsError =
monitorFields.locations &&
monitorFields.locations.length === 0 &&
@ -98,7 +92,7 @@ export function validateProjectMonitor(
? 'Invalid value "[]" supplied to field "locations"'
: '';
// Cast it to ICMPCodec to satisfy typing. During runtime, correct codec will be used to decode.
const decodedMonitor = ProjectBrowserMonitorCodec.decode(monitorFields);
const decodedMonitor = ProjectMonitorCodec.decode(monitorFields);
if (isLeft(decodedMonitor)) {
return {

View file

@ -66,7 +66,6 @@ export const browserFormatters: BrowserFormatMap = {
[ConfigKey.JOURNEY_FILTERS_TAGS]: (fields) =>
arrayFormatter(fields[ConfigKey.JOURNEY_FILTERS_TAGS]),
[ConfigKey.IGNORE_HTTPS_ERRORS]: null,
[ConfigKey.JOURNEY_ID]: null,
[ConfigKey.PROJECT_ID]: null,
[ConfigKey.PLAYWRIGHT_OPTIONS]: (fields) =>
stringToObjectFormatter(fields[ConfigKey.PLAYWRIGHT_OPTIONS] || ''),

View file

@ -30,6 +30,7 @@ export const commonFormatters: CommonFormatMap = {
[ConfigKey.MONITOR_SOURCE_TYPE]: (fields) =>
fields[ConfigKey.MONITOR_SOURCE_TYPE] || SourceType.UI,
[ConfigKey.FORM_MONITOR_TYPE]: null,
[ConfigKey.JOURNEY_ID]: null,
};
export const arrayFormatter = (value: string[] = []) => (value.length ? value : null);

View file

@ -1,267 +0,0 @@
/*
* 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 {
DataStream,
ScreenshotOption,
Locations,
LocationStatus,
ProjectBrowserMonitor,
PrivateLocation,
} from '../../../common/runtime_types';
import { DEFAULT_FIELDS } from '../../../common/constants/monitor_defaults';
import { normalizeProjectMonitors } from './browser';
describe('browser normalizers', () => {
describe('normalize push monitors', () => {
const playwrightOptions = {
headless: true,
};
const params = {
url: 'test-url',
};
const projectId = 'test-project-id';
const locations: Locations = [
{
id: 'us_central',
label: 'Test Location',
geo: { lat: 33.333, lon: 73.333 },
url: 'test-url',
isServiceManaged: true,
status: LocationStatus.GA,
},
{
id: 'us_east',
label: 'Test Location',
geo: { lat: 33.333, lon: 73.333 },
url: 'test-url',
isServiceManaged: true,
status: LocationStatus.GA,
},
];
const privateLocations: PrivateLocation[] = [
{
id: 'germany',
label: 'Germany',
concurrentMonitors: 1,
agentPolicyId: 'germany',
},
];
const monitors: ProjectBrowserMonitor[] = [
{
id: 'test-id-1',
screenshot: ScreenshotOption.OFF,
name: 'test-name-1',
content: 'test content 1',
schedule: 3,
throttling: {
latency: 20,
upload: 10,
download: 5,
},
locations: ['us_central'],
tags: ['tag1', 'tag2'],
ignoreHTTPSErrors: true,
apmServiceName: 'cart-service',
},
{
id: 'test-id-2',
screenshot: ScreenshotOption.ON,
name: 'test-name-2',
content: 'test content 2',
schedule: 10,
throttling: {
latency: 18,
upload: 15,
download: 10,
},
params: {},
playwrightOptions: {},
locations: ['us_central', 'us_east'],
tags: ['tag3', 'tag4'],
ignoreHTTPSErrors: false,
apmServiceName: 'bean-service',
},
{
id: 'test-id-3',
screenshot: ScreenshotOption.ON,
name: 'test-name-3',
content: 'test content 3',
schedule: 10,
throttling: {
latency: 18,
upload: 15,
download: 10,
},
params,
playwrightOptions,
locations: ['us_central', 'us_east'],
privateLocations: ['Germany'],
tags: ['tag3', 'tag4'],
ignoreHTTPSErrors: false,
apmServiceName: 'bean-service',
},
];
it('properly normalizes browser monitor', () => {
const actual = normalizeProjectMonitors({
locations,
privateLocations,
monitors,
projectId,
namespace: 'test-space',
});
expect(actual).toEqual([
{
...DEFAULT_FIELDS[DataStream.BROWSER],
journey_id: 'test-id-1',
ignore_https_errors: true,
origin: 'project',
locations: [
{
geo: {
lat: 33.333,
lon: 73.333,
},
id: 'us_central',
isServiceManaged: true,
label: 'Test Location',
url: 'test-url',
status: 'ga',
},
],
name: 'test-name-1',
schedule: {
number: '3',
unit: 'm',
},
screenshots: 'off',
'service.name': 'cart-service',
'source.project.content': 'test content 1',
tags: ['tag1', 'tag2'],
'throttling.config': '5d/10u/20l',
'throttling.download_speed': '5',
'throttling.is_enabled': true,
'throttling.latency': '20',
'throttling.upload_speed': '10',
params: '',
type: 'browser',
project_id: projectId,
namespace: 'test_space',
original_space: 'test-space',
custom_heartbeat_id: 'test-id-1-test-project-id-test-space',
timeout: null,
},
{
...DEFAULT_FIELDS[DataStream.BROWSER],
journey_id: 'test-id-2',
ignore_https_errors: false,
origin: 'project',
locations: [
{
geo: {
lat: 33.333,
lon: 73.333,
},
id: 'us_central',
isServiceManaged: true,
label: 'Test Location',
url: 'test-url',
status: 'ga',
},
{
geo: {
lat: 33.333,
lon: 73.333,
},
id: 'us_east',
isServiceManaged: true,
label: 'Test Location',
url: 'test-url',
status: 'ga',
},
],
name: 'test-name-2',
params: '',
playwright_options: '',
schedule: {
number: '10',
unit: 'm',
},
screenshots: 'on',
'service.name': 'bean-service',
'source.project.content': 'test content 2',
tags: ['tag3', 'tag4'],
'throttling.config': '10d/15u/18l',
'throttling.download_speed': '10',
'throttling.is_enabled': true,
'throttling.latency': '18',
'throttling.upload_speed': '15',
type: 'browser',
project_id: projectId,
namespace: 'test_space',
original_space: 'test-space',
custom_heartbeat_id: 'test-id-2-test-project-id-test-space',
timeout: null,
},
{
...DEFAULT_FIELDS[DataStream.BROWSER],
journey_id: 'test-id-3',
ignore_https_errors: false,
origin: 'project',
locations: [
{
geo: {
lat: 33.333,
lon: 73.333,
},
id: 'us_central',
isServiceManaged: true,
label: 'Test Location',
url: 'test-url',
status: 'ga',
},
{
geo: {
lat: 33.333,
lon: 73.333,
},
id: 'us_east',
isServiceManaged: true,
label: 'Test Location',
url: 'test-url',
status: 'ga',
},
privateLocations[0],
],
name: 'test-name-3',
params: JSON.stringify(params),
playwright_options: JSON.stringify(playwrightOptions),
schedule: {
number: '10',
unit: 'm',
},
screenshots: 'on',
'service.name': 'bean-service',
'source.project.content': 'test content 3',
tags: ['tag3', 'tag4'],
'throttling.config': '10d/15u/18l',
'throttling.download_speed': '10',
'throttling.is_enabled': true,
'throttling.latency': '18',
'throttling.upload_speed': '15',
type: 'browser',
project_id: projectId,
namespace: 'test_space',
original_space: 'test-space',
custom_heartbeat_id: 'test-id-3-test-project-id-test-space',
timeout: null,
},
]);
});
});
});

View file

@ -1,157 +0,0 @@
/*
* 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 { PrivateLocation } from '../../../common/runtime_types';
import { DEFAULT_FIELDS } from '../../../common/constants/monitor_defaults';
import { formatKibanaNamespace } from '../../../common/formatters';
import {
BrowserFields,
ConfigKey,
DataStream,
FormMonitorType,
Locations,
ProjectBrowserMonitor,
ScheduleUnit,
SourceType,
} from '../../../common/runtime_types/monitor_management';
/* Represents all of the push-monitor related fields that need to be
* normalized. Excludes fields that we do not support for push monitors
* This type ensures that contributors remember to add normalizers for push
* monitors where appropriate when new keys are added to browser montiors */
type NormalizedPublicFields = Omit<
BrowserFields,
| ConfigKey.METADATA
| ConfigKey.SOURCE_INLINE
| ConfigKey.SOURCE_ZIP_URL
| ConfigKey.SOURCE_ZIP_USERNAME
| ConfigKey.SOURCE_ZIP_PASSWORD
| ConfigKey.SOURCE_ZIP_FOLDER
| ConfigKey.SOURCE_ZIP_PROXY_URL
| ConfigKey.ZIP_URL_TLS_CERTIFICATE_AUTHORITIES
| ConfigKey.ZIP_URL_TLS_CERTIFICATE
| ConfigKey.ZIP_URL_TLS_KEY
| ConfigKey.ZIP_URL_TLS_KEY_PASSPHRASE
| ConfigKey.ZIP_URL_TLS_VERIFICATION_MODE
| ConfigKey.ZIP_URL_TLS_VERSION
| ConfigKey.JOURNEY_FILTERS_TAGS
| ConfigKey.SYNTHETICS_ARGS
| ConfigKey.PORT
| ConfigKey.URLS
>;
export const normalizeProjectMonitor = ({
locations = [],
privateLocations = [],
monitor,
projectId,
namespace,
}: {
locations: Locations;
privateLocations: PrivateLocation[];
monitor: ProjectBrowserMonitor;
projectId: string;
namespace: string;
}): BrowserFields => {
const defaultFields = DEFAULT_FIELDS[DataStream.BROWSER];
const normalizedFields: NormalizedPublicFields = {
[ConfigKey.MONITOR_TYPE]: DataStream.BROWSER,
[ConfigKey.FORM_MONITOR_TYPE]: FormMonitorType.MULTISTEP,
[ConfigKey.MONITOR_SOURCE_TYPE]: SourceType.PROJECT,
[ConfigKey.NAME]: monitor.name || '',
[ConfigKey.SCHEDULE]: {
number: `${monitor.schedule}`,
unit: ScheduleUnit.MINUTES,
},
[ConfigKey.PROJECT_ID]: projectId || defaultFields[ConfigKey.PROJECT_ID],
[ConfigKey.JOURNEY_ID]: monitor.id || defaultFields[ConfigKey.JOURNEY_ID],
[ConfigKey.SOURCE_PROJECT_CONTENT]:
monitor.content || defaultFields[ConfigKey.SOURCE_PROJECT_CONTENT],
[ConfigKey.LOCATIONS]: getMonitorLocations({
monitor,
privateLocations,
publicLocations: locations,
}),
[ConfigKey.THROTTLING_CONFIG]: monitor.throttling
? `${monitor.throttling.download}d/${monitor.throttling.upload}u/${monitor.throttling.latency}l`
: defaultFields[ConfigKey.THROTTLING_CONFIG],
[ConfigKey.DOWNLOAD_SPEED]: `${
monitor.throttling?.download || defaultFields[ConfigKey.DOWNLOAD_SPEED]
}`,
[ConfigKey.UPLOAD_SPEED]: `${
monitor.throttling?.upload || defaultFields[ConfigKey.UPLOAD_SPEED]
}`,
[ConfigKey.IS_THROTTLING_ENABLED]:
Boolean(monitor.throttling) || defaultFields[ConfigKey.IS_THROTTLING_ENABLED],
[ConfigKey.LATENCY]: `${monitor.throttling?.latency || defaultFields[ConfigKey.LATENCY]}`,
[ConfigKey.APM_SERVICE_NAME]:
monitor.apmServiceName || defaultFields[ConfigKey.APM_SERVICE_NAME],
[ConfigKey.IGNORE_HTTPS_ERRORS]:
monitor.ignoreHTTPSErrors || defaultFields[ConfigKey.IGNORE_HTTPS_ERRORS],
[ConfigKey.SCREENSHOTS]: monitor.screenshot || defaultFields[ConfigKey.SCREENSHOTS],
[ConfigKey.TAGS]: monitor.tags || defaultFields[ConfigKey.TAGS],
[ConfigKey.PLAYWRIGHT_OPTIONS]: Object.keys(monitor.playwrightOptions || {}).length
? JSON.stringify(monitor.playwrightOptions)
: defaultFields[ConfigKey.PLAYWRIGHT_OPTIONS],
[ConfigKey.PARAMS]: Object.keys(monitor.params || {}).length
? JSON.stringify(monitor.params)
: defaultFields[ConfigKey.PARAMS],
[ConfigKey.JOURNEY_FILTERS_MATCH]:
monitor.filter?.match || defaultFields[ConfigKey.JOURNEY_FILTERS_MATCH],
[ConfigKey.NAMESPACE]: formatKibanaNamespace(namespace) || defaultFields[ConfigKey.NAMESPACE],
[ConfigKey.ORIGINAL_SPACE]: namespace || defaultFields[ConfigKey.ORIGINAL_SPACE],
[ConfigKey.CUSTOM_HEARTBEAT_ID]: `${monitor.id}-${projectId}-${namespace}`,
[ConfigKey.TIMEOUT]: null,
[ConfigKey.ENABLED]: monitor.enabled ?? defaultFields[ConfigKey.ENABLED],
};
return {
...DEFAULT_FIELDS[DataStream.BROWSER],
...normalizedFields,
};
};
export const normalizeProjectMonitors = ({
locations = [],
privateLocations = [],
monitors = [],
projectId,
namespace,
}: {
locations: Locations;
privateLocations: PrivateLocation[];
monitors: ProjectBrowserMonitor[];
projectId: string;
namespace: string;
}) => {
return monitors.map((monitor) => {
return normalizeProjectMonitor({ monitor, locations, privateLocations, projectId, namespace });
});
};
export const getMonitorLocations = ({
privateLocations,
publicLocations,
monitor,
}: {
monitor: ProjectBrowserMonitor;
privateLocations: PrivateLocation[];
publicLocations: Locations;
}) => {
const publicLocs =
monitor.locations?.map((id) => {
return publicLocations.find((location) => location.id === id);
}) || [];
const privateLocs =
monitor.privateLocations?.map((locationName) => {
return privateLocations.find(
(location) => location.label.toLowerCase() === locationName.toLowerCase()
);
}) || [];
return [...publicLocs, ...privateLocs].filter(
(location) => location !== undefined
) as BrowserFields[ConfigKey.LOCATIONS];
};

View file

@ -7,6 +7,7 @@
import { KibanaRequest, SavedObjectsClientContract } from '@kbn/core/server';
import { NewPackagePolicy } from '@kbn/fleet-plugin/common';
import { NewPackagePolicyWithId } from '@kbn/fleet-plugin/server/services/package_policy';
import { cloneDeep } from 'lodash';
import { formatSyntheticsPolicy } from '../../../common/formatters/format_synthetics_policy';
import { getSyntheticsPrivateLocations } from '../../legacy_uptime/lib/saved_objects/private_locations';
import {
@ -55,7 +56,7 @@ export class SyntheticsPrivateLocation {
const { label: locName } = privateLocation;
const newPolicy = { ...newPolicyTemplate };
const newPolicy = cloneDeep(newPolicyTemplate);
try {
newPolicy.is_managed = true;

View file

@ -0,0 +1,286 @@
/*
* 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 {
DataStream,
ScreenshotOption,
Locations,
LocationStatus,
ProjectMonitor,
PrivateLocation,
} from '../../../../common/runtime_types';
import { DEFAULT_FIELDS } from '../../../../common/constants/monitor_defaults';
import { normalizeProjectMonitors } from '.';
describe('browser normalizers', () => {
describe('normalize push monitors', () => {
const playwrightOptions = {
headless: true,
};
const params = {
url: 'test-url',
};
const projectId = 'test-project-id';
const locations: Locations = [
{
id: 'us_central',
label: 'Test Location',
geo: { lat: 33.333, lon: 73.333 },
url: 'test-url',
isServiceManaged: true,
status: LocationStatus.GA,
},
{
id: 'us_east',
label: 'Test Location',
geo: { lat: 33.333, lon: 73.333 },
url: 'test-url',
isServiceManaged: true,
status: LocationStatus.GA,
},
];
const privateLocations: PrivateLocation[] = [
{
id: 'germany',
label: 'Germany',
isServiceManaged: false,
concurrentMonitors: 1,
agentPolicyId: 'germany',
},
];
const monitors: ProjectMonitor[] = [
{
id: 'test-id-1',
screenshot: ScreenshotOption.OFF,
name: 'test-name-1',
content: 'test content 1',
schedule: 3,
throttling: {
latency: 20,
upload: 10,
download: 5,
},
locations: ['us_central'],
tags: ['tag1', 'tag2'],
ignoreHTTPSErrors: true,
apmServiceName: 'cart-service',
type: DataStream.BROWSER,
},
{
id: 'test-id-2',
screenshot: ScreenshotOption.ON,
name: 'test-name-2',
content: 'test content 2',
schedule: 10,
throttling: {
latency: 18,
upload: 15,
download: 10,
},
params: {},
playwrightOptions: {},
locations: ['us_central', 'us_east'],
tags: ['tag3', 'tag4'],
ignoreHTTPSErrors: false,
apmServiceName: 'bean-service',
type: DataStream.BROWSER,
},
{
id: 'test-id-3',
screenshot: ScreenshotOption.ON,
name: 'test-name-3',
content: 'test content 3',
schedule: 10,
throttling: {
latency: 18,
upload: 15,
download: 10,
},
params,
playwrightOptions,
locations: ['us_central', 'us_east'],
privateLocations: ['Germany'],
tags: ['tag3', 'tag4'],
ignoreHTTPSErrors: false,
apmServiceName: 'bean-service',
type: DataStream.BROWSER,
},
];
it('properly normalizes browser monitor', () => {
const actual = normalizeProjectMonitors({
locations,
privateLocations,
monitors,
projectId,
namespace: 'test-space',
});
expect(actual).toEqual([
{
normalizedFields: {
...DEFAULT_FIELDS[DataStream.BROWSER],
journey_id: 'test-id-1',
ignore_https_errors: true,
origin: 'project',
locations: [
{
geo: {
lat: 33.333,
lon: 73.333,
},
id: 'us_central',
isServiceManaged: true,
label: 'Test Location',
url: 'test-url',
status: 'ga',
},
],
name: 'test-name-1',
schedule: {
number: '3',
unit: 'm',
},
screenshots: 'off',
'service.name': 'cart-service',
'source.project.content': 'test content 1',
tags: ['tag1', 'tag2'],
'throttling.config': '5d/10u/20l',
'throttling.download_speed': '5',
'throttling.is_enabled': true,
'throttling.latency': '20',
'throttling.upload_speed': '10',
params: '',
type: 'browser',
project_id: projectId,
namespace: 'test_space',
original_space: 'test-space',
custom_heartbeat_id: 'test-id-1-test-project-id-test-space',
timeout: null,
},
unsupportedKeys: [],
},
{
normalizedFields: {
...DEFAULT_FIELDS[DataStream.BROWSER],
journey_id: 'test-id-2',
ignore_https_errors: false,
origin: 'project',
locations: [
{
geo: {
lat: 33.333,
lon: 73.333,
},
id: 'us_central',
isServiceManaged: true,
label: 'Test Location',
url: 'test-url',
status: 'ga',
},
{
geo: {
lat: 33.333,
lon: 73.333,
},
id: 'us_east',
isServiceManaged: true,
label: 'Test Location',
url: 'test-url',
status: 'ga',
},
],
name: 'test-name-2',
params: '',
playwright_options: '',
schedule: {
number: '10',
unit: 'm',
},
screenshots: 'on',
'service.name': 'bean-service',
'source.project.content': 'test content 2',
tags: ['tag3', 'tag4'],
'throttling.config': '10d/15u/18l',
'throttling.download_speed': '10',
'throttling.is_enabled': true,
'throttling.latency': '18',
'throttling.upload_speed': '15',
type: 'browser',
project_id: projectId,
namespace: 'test_space',
original_space: 'test-space',
custom_heartbeat_id: 'test-id-2-test-project-id-test-space',
timeout: null,
},
unsupportedKeys: [],
},
{
normalizedFields: {
...DEFAULT_FIELDS[DataStream.BROWSER],
journey_id: 'test-id-3',
ignore_https_errors: false,
origin: 'project',
locations: [
{
geo: {
lat: 33.333,
lon: 73.333,
},
id: 'us_central',
isServiceManaged: true,
label: 'Test Location',
url: 'test-url',
status: 'ga',
},
{
geo: {
lat: 33.333,
lon: 73.333,
},
id: 'us_east',
isServiceManaged: true,
label: 'Test Location',
url: 'test-url',
status: 'ga',
},
{
id: 'germany',
isServiceManaged: false,
label: 'Germany',
agentPolicyId: 'germany',
concurrentMonitors: 1,
},
],
name: 'test-name-3',
params: JSON.stringify(params),
playwright_options: JSON.stringify(playwrightOptions),
schedule: {
number: '10',
unit: 'm',
},
screenshots: 'on',
'service.name': 'bean-service',
'source.project.content': 'test content 3',
tags: ['tag3', 'tag4'],
'throttling.config': '10d/15u/18l',
'throttling.download_speed': '10',
'throttling.is_enabled': true,
'throttling.latency': '18',
'throttling.upload_speed': '15',
type: 'browser',
project_id: projectId,
namespace: 'test_space',
original_space: 'test-space',
custom_heartbeat_id: 'test-id-3-test-project-id-test-space',
timeout: null,
},
unsupportedKeys: [],
},
]);
});
});
});

View file

@ -0,0 +1,85 @@
/*
* 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 {
BrowserFields,
ConfigKey,
DataStream,
FormMonitorType,
Locations,
PrivateLocation,
ProjectMonitor,
} from '../../../../common/runtime_types';
import { getNormalizeCommonFields, getValueInSeconds } from './common_fields';
import { DEFAULT_FIELDS } from '../../../../common/constants/monitor_defaults';
export interface NormalizedProjectProps {
locations: Locations;
privateLocations: PrivateLocation[];
monitor: ProjectMonitor;
projectId: string;
namespace: string;
}
export const getNormalizeBrowserFields = ({
locations = [],
privateLocations = [],
monitor,
projectId,
namespace,
}: NormalizedProjectProps): { normalizedFields: BrowserFields; unsupportedKeys: string[] } => {
const defaultFields = DEFAULT_FIELDS[DataStream.BROWSER];
const commonFields = getNormalizeCommonFields({
locations,
privateLocations,
monitor,
projectId,
namespace,
});
const normalizedFields = {
...commonFields,
[ConfigKey.MONITOR_TYPE]: DataStream.BROWSER,
[ConfigKey.FORM_MONITOR_TYPE]: FormMonitorType.MULTISTEP,
[ConfigKey.SOURCE_PROJECT_CONTENT]:
monitor.content || defaultFields[ConfigKey.SOURCE_PROJECT_CONTENT],
[ConfigKey.THROTTLING_CONFIG]: monitor.throttling
? `${monitor.throttling.download}d/${monitor.throttling.upload}u/${monitor.throttling.latency}l`
: defaultFields[ConfigKey.THROTTLING_CONFIG],
[ConfigKey.DOWNLOAD_SPEED]: `${
monitor.throttling?.download || defaultFields[ConfigKey.DOWNLOAD_SPEED]
}`,
[ConfigKey.UPLOAD_SPEED]: `${
monitor.throttling?.upload || defaultFields[ConfigKey.UPLOAD_SPEED]
}`,
[ConfigKey.IS_THROTTLING_ENABLED]:
Boolean(monitor.throttling) || defaultFields[ConfigKey.IS_THROTTLING_ENABLED],
[ConfigKey.LATENCY]: `${monitor.throttling?.latency || defaultFields[ConfigKey.LATENCY]}`,
[ConfigKey.IGNORE_HTTPS_ERRORS]:
monitor.ignoreHTTPSErrors || defaultFields[ConfigKey.IGNORE_HTTPS_ERRORS],
[ConfigKey.SCREENSHOTS]: monitor.screenshot || defaultFields[ConfigKey.SCREENSHOTS],
[ConfigKey.PLAYWRIGHT_OPTIONS]: Object.keys(monitor.playwrightOptions || {}).length
? JSON.stringify(monitor.playwrightOptions)
: defaultFields[ConfigKey.PLAYWRIGHT_OPTIONS],
[ConfigKey.PARAMS]: Object.keys(monitor.params || {}).length
? JSON.stringify(monitor.params)
: defaultFields[ConfigKey.PARAMS],
[ConfigKey.JOURNEY_FILTERS_MATCH]:
monitor.filter?.match || defaultFields[ConfigKey.JOURNEY_FILTERS_MATCH],
[ConfigKey.TIMEOUT]: monitor.timeout
? getValueInSeconds(monitor.timeout)
: defaultFields[ConfigKey.TIMEOUT],
};
return {
normalizedFields: {
...defaultFields,
...normalizedFields,
},
unsupportedKeys: [],
};
};

View file

@ -0,0 +1,184 @@
/*
* 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 { omit } from 'lodash';
import { formatKibanaNamespace } from '../../../../common/formatters';
import {
BrowserFields,
ConfigKey,
CommonFields,
DataStream,
PrivateLocation,
Locations,
ProjectMonitor,
ScheduleUnit,
SourceType,
} from '../../../../common/runtime_types';
import { DEFAULT_FIELDS } from '../../../../common/constants/monitor_defaults';
import { DEFAULT_COMMON_FIELDS } from '../../../../common/constants/monitor_defaults';
import { NormalizedProjectProps } from '.';
export const getNormalizeCommonFields = ({
locations = [],
privateLocations = [],
monitor,
projectId,
namespace,
}: NormalizedProjectProps): CommonFields => {
const defaultFields = DEFAULT_COMMON_FIELDS;
const normalizedFields = {
[ConfigKey.JOURNEY_ID]: monitor.id || defaultFields[ConfigKey.JOURNEY_ID],
[ConfigKey.MONITOR_SOURCE_TYPE]: SourceType.PROJECT,
[ConfigKey.NAME]: monitor.name || '',
[ConfigKey.SCHEDULE]: {
number: `${monitor.schedule}`,
unit: ScheduleUnit.MINUTES,
},
[ConfigKey.PROJECT_ID]: projectId,
[ConfigKey.LOCATIONS]: getMonitorLocations({
monitor,
privateLocations,
publicLocations: locations,
}),
[ConfigKey.APM_SERVICE_NAME]:
monitor.apmServiceName || defaultFields[ConfigKey.APM_SERVICE_NAME],
[ConfigKey.TAGS]: getOptionalListField(monitor.tags) || defaultFields[ConfigKey.TAGS],
[ConfigKey.NAMESPACE]: formatKibanaNamespace(namespace) || defaultFields[ConfigKey.NAMESPACE],
[ConfigKey.ORIGINAL_SPACE]: namespace || defaultFields[ConfigKey.NAMESPACE],
[ConfigKey.CUSTOM_HEARTBEAT_ID]: getCustomHeartbeatId(monitor, projectId, namespace),
[ConfigKey.ENABLED]: monitor.enabled ?? defaultFields[ConfigKey.ENABLED],
};
return {
...defaultFields,
...normalizedFields,
};
};
export const getCustomHeartbeatId = (
monitor: NormalizedProjectProps['monitor'],
projectId: string,
namespace: string
) => {
return `${monitor.id}-${projectId}-${namespace}`;
};
export const getMonitorLocations = ({
privateLocations,
publicLocations,
monitor,
}: {
monitor: ProjectMonitor;
privateLocations: PrivateLocation[];
publicLocations: Locations;
}) => {
const publicLocs =
monitor.locations?.map((id) => {
return publicLocations.find((location) => location.id === id);
}) || [];
const privateLocs =
monitor.privateLocations?.map((locationName) => {
return privateLocations.find(
(location) =>
location.label.toLowerCase() === locationName.toLowerCase() ||
location.id.toLowerCase() === locationName.toLowerCase()
);
}) || [];
return [...publicLocs, ...privateLocs].filter(
(location) => location !== undefined
) as BrowserFields[ConfigKey.LOCATIONS];
};
export const getValueInSeconds = (value: string) => {
const keyMap = {
h: 60 * 60,
m: 60,
s: 1,
};
const key = value.slice(-1) as 'h' | 'm' | 's';
const time = parseInt(value.slice(0, -1), 10);
const valueInSeconds = time * (keyMap[key] || 1);
return typeof valueInSeconds === 'number' ? `${valueInSeconds}` : null;
};
/**
* Accounts for array values that are optionally defined as a comma seperated list
*
* @param {Array | string} [value]
* @returns {array} Returns an array
*/
export const getOptionalListField = (value?: string[] | string): string[] => {
if (Array.isArray(value)) {
return value;
}
return value ? value.split(',') : [];
};
/**
* Accounts for heartbeat fields that are optionally an array or single string
*
* @param {Array | string} [value]
* @returns {string} Returns first item when the value is an array, or the value itself
*/
export const getOptionalArrayField = (value: string[] | string = '') => {
const array = getOptionalListField(value);
return array[0];
};
/**
* Flattens arbitrary yaml into a synthetics monitor compatible configuration
*
* @param {Object} [monitor]
* @returns {Object} Returns an object containing synthetics-compatible configuration keys
*/
const flattenAndFormatObject = (obj: Record<string, unknown>, prefix = '', keys: string[]) =>
Object.keys(obj).reduce<Record<string, unknown>>((acc, k) => {
const pre = prefix.length ? prefix + '.' : '';
const key = pre + k;
/* If the key is an array of numbers, convert to an array of strings */
if (Array.isArray(obj[k])) {
acc[key] = (obj[k] as unknown[]).map((value) =>
typeof value === 'number' ? String(value) : value
);
return acc;
}
/* if the key is a supported key stop flattening early */
if (keys.includes(key)) {
acc[key] = obj[k];
return acc;
}
if (typeof obj[k] === 'object') {
Object.assign(acc, flattenAndFormatObject(obj[k] as Record<string, unknown>, pre + k, keys));
} else {
acc[key] = obj[k];
}
return acc;
}, {});
export const normalizeYamlConfig = (monitor: NormalizedProjectProps['monitor']) => {
const defaultFields = DEFAULT_FIELDS[monitor.type as DataStream];
const supportedKeys = Object.keys(defaultFields);
const flattenedConfig = flattenAndFormatObject(monitor, '', supportedKeys);
const {
locations: _locations,
privateLocations: _privateLocations,
content: _content,
id: _id,
...yamlConfig
} = flattenedConfig;
const unsupportedKeys = Object.keys(yamlConfig).filter((key) => !supportedKeys.includes(key));
const supportedYamlConfig = omit(yamlConfig, unsupportedKeys);
return {
yamlConfig: supportedYamlConfig,
unsupportedKeys,
};
};

View file

@ -0,0 +1,55 @@
/*
* 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 { getNormalizeCommonFields } from './common_fields';
import { NormalizedProjectProps } from './browser_monitor';
import { DEFAULT_FIELDS } from '../../../../common/constants/monitor_defaults';
import {
ConfigKey,
DataStream,
FormMonitorType,
HTTPFields,
} from '../../../../common/runtime_types/monitor_management';
import { normalizeYamlConfig, getValueInSeconds, getOptionalArrayField } from './common_fields';
export const getNormalizeHTTPFields = ({
locations = [],
privateLocations = [],
monitor,
projectId,
namespace,
}: NormalizedProjectProps): { normalizedFields: HTTPFields; unsupportedKeys: string[] } => {
const defaultFields = DEFAULT_FIELDS[DataStream.HTTP];
const { yamlConfig, unsupportedKeys } = normalizeYamlConfig(monitor);
const commonFields = getNormalizeCommonFields({
locations,
privateLocations,
monitor,
projectId,
namespace,
});
const normalizedFields = {
...yamlConfig,
...commonFields,
[ConfigKey.MONITOR_TYPE]: DataStream.HTTP,
[ConfigKey.FORM_MONITOR_TYPE]: FormMonitorType.HTTP,
[ConfigKey.URLS]: getOptionalArrayField(monitor.urls) || defaultFields[ConfigKey.URLS],
[ConfigKey.MAX_REDIRECTS]:
monitor[ConfigKey.MAX_REDIRECTS] || defaultFields[ConfigKey.MAX_REDIRECTS],
[ConfigKey.TIMEOUT]: monitor.timeout
? getValueInSeconds(monitor.timeout)
: defaultFields[ConfigKey.TIMEOUT],
};
return {
normalizedFields: {
...defaultFields,
...normalizedFields,
},
unsupportedKeys,
};
};

View file

@ -0,0 +1,58 @@
/*
* 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 { getNormalizeCommonFields } from './common_fields';
import { NormalizedProjectProps } from './browser_monitor';
import { DEFAULT_FIELDS } from '../../../../common/constants/monitor_defaults';
import {
ConfigKey,
DataStream,
FormMonitorType,
ICMPFields,
} from '../../../../common/runtime_types/monitor_management';
import { normalizeYamlConfig, getValueInSeconds, getOptionalArrayField } from './common_fields';
export const getNormalizeICMPFields = ({
locations = [],
privateLocations = [],
monitor,
projectId,
namespace,
}: NormalizedProjectProps): { normalizedFields: ICMPFields; unsupportedKeys: string[] } => {
const defaultFields = DEFAULT_FIELDS[DataStream.ICMP];
const { yamlConfig, unsupportedKeys } = normalizeYamlConfig(monitor);
const commonFields = getNormalizeCommonFields({
locations,
privateLocations,
monitor,
projectId,
namespace,
});
const normalizedFields = {
...yamlConfig,
...commonFields,
[ConfigKey.MONITOR_TYPE]: DataStream.ICMP,
[ConfigKey.FORM_MONITOR_TYPE]: FormMonitorType.ICMP,
[ConfigKey.HOSTS]:
getOptionalArrayField(monitor[ConfigKey.HOSTS]) || defaultFields[ConfigKey.HOSTS],
[ConfigKey.TIMEOUT]: monitor.timeout
? getValueInSeconds(monitor.timeout)
: defaultFields[ConfigKey.TIMEOUT],
[ConfigKey.WAIT]: monitor.wait
? getValueInSeconds(monitor.wait) || defaultFields[ConfigKey.WAIT]
: defaultFields[ConfigKey.WAIT],
};
return {
normalizedFields: {
...defaultFields,
...normalizedFields,
},
unsupportedKeys,
};
};

View file

@ -0,0 +1,63 @@
/*
* 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 {
DataStream,
PrivateLocation,
Locations,
ProjectMonitor,
} from '../../../../common/runtime_types';
import { getNormalizeBrowserFields } from './browser_monitor';
import { getNormalizeICMPFields } from './icmp_monitor';
import { getNormalizeTCPFields } from './tcp_monitor';
import { getNormalizeHTTPFields } from './http_monitor';
export interface NormalizedProjectProps {
locations: Locations;
privateLocations: PrivateLocation[];
monitor: ProjectMonitor;
projectId: string;
namespace: string;
}
export const normalizeProjectMonitor = (props: NormalizedProjectProps) => {
const { monitor } = props;
const type = monitor.type || DataStream.BROWSER;
switch (type) {
case DataStream.BROWSER:
return getNormalizeBrowserFields(props);
case DataStream.HTTP:
return getNormalizeHTTPFields(props);
case DataStream.TCP:
return getNormalizeTCPFields(props);
case DataStream.ICMP:
return getNormalizeICMPFields(props);
default:
throw new Error(`Unsupported monitor type ${monitor.type}`);
}
};
export const normalizeProjectMonitors = ({
locations = [],
privateLocations = [],
monitors = [],
projectId,
namespace,
}: {
locations: Locations;
privateLocations: PrivateLocation[];
monitors: ProjectMonitor[];
projectId: string;
namespace: string;
}) => {
return monitors.map((monitor) => {
return normalizeProjectMonitor({ monitor, locations, privateLocations, projectId, namespace });
});
};

View file

@ -0,0 +1,56 @@
/*
* 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 { NormalizedProjectProps } from './browser_monitor';
import { DEFAULT_FIELDS } from '../../../../common/constants/monitor_defaults';
import { normalizeYamlConfig, getValueInSeconds } from './common_fields';
import {
ConfigKey,
DataStream,
FormMonitorType,
TCPFields,
} from '../../../../common/runtime_types/monitor_management';
import { getNormalizeCommonFields, getOptionalArrayField } from './common_fields';
export const getNormalizeTCPFields = ({
locations = [],
privateLocations = [],
monitor,
projectId,
namespace,
}: NormalizedProjectProps): { normalizedFields: TCPFields; unsupportedKeys: string[] } => {
const defaultFields = DEFAULT_FIELDS[DataStream.TCP];
const { yamlConfig, unsupportedKeys } = normalizeYamlConfig(monitor);
const commonFields = getNormalizeCommonFields({
locations,
privateLocations,
monitor,
projectId,
namespace,
});
const normalizedFields = {
...yamlConfig,
...commonFields,
[ConfigKey.MONITOR_TYPE]: DataStream.TCP,
[ConfigKey.FORM_MONITOR_TYPE]: FormMonitorType.TCP,
[ConfigKey.HOSTS]:
getOptionalArrayField(monitor[ConfigKey.HOSTS]) || defaultFields[ConfigKey.HOSTS],
[ConfigKey.TIMEOUT]: monitor.timeout
? getValueInSeconds(monitor.timeout)
: defaultFields[ConfigKey.TIMEOUT],
};
return {
normalizedFields: {
...defaultFields,
...normalizedFields,
},
unsupportedKeys,
};
};

View file

@ -10,20 +10,21 @@ import {
INSUFFICIENT_FLEET_PERMISSIONS,
ProjectMonitorFormatter,
} from './project_monitor_formatter';
import { LocationStatus } from '../../common/runtime_types';
import { LocationStatus } from '../../../common/runtime_types';
import { times } from 'lodash';
import { SyntheticsService } from './synthetics_service';
import { UptimeServerSetup } from '../legacy_uptime/lib/adapters';
import { SyntheticsService } from '../synthetics_service';
import { UptimeServerSetup } from '../../legacy_uptime/lib/adapters';
import { encryptedSavedObjectsMock } from '@kbn/encrypted-saved-objects-plugin/server/mocks';
import { SyntheticsMonitorClient } from './synthetics_monitor/synthetics_monitor_client';
import { SyntheticsMonitorClient } from '../synthetics_monitor/synthetics_monitor_client';
import { httpServerMock } from '@kbn/core-http-server-mocks';
import { Subject } from 'rxjs';
import { formatSecrets } from './utils';
import { formatSecrets } from '../utils';
import * as telemetryHooks from '../routes/telemetry/monitor_upgrade_sender';
import * as telemetryHooks from '../../routes/telemetry/monitor_upgrade_sender';
const testMonitors = [
{
type: 'browser',
throttling: { download: 5, upload: 3, latency: 20 },
schedule: 3,
locations: [],
@ -46,6 +47,7 @@ const testMonitors = [
filter: { match: 'check if title is present 10 0' },
},
{
type: 'browser',
throttling: { download: 5, upload: 3, latency: 20 },
schedule: 3,
locations: [],

View file

@ -13,30 +13,34 @@ import {
SavedObjectsFindResult,
} from '@kbn/core/server';
import { EncryptedSavedObjectsClient } from '@kbn/encrypted-saved-objects-plugin/server';
import { syncNewMonitorBulk } from '../routes/monitor_cruds/bulk_cruds/add_monitor_bulk';
import { deleteMonitorBulk } from '../routes/monitor_cruds/bulk_cruds/delete_monitor_bulk';
import { SyntheticsMonitorClient } from './synthetics_monitor/synthetics_monitor_client';
import { syncNewMonitorBulk } from '../../routes/monitor_cruds/bulk_cruds/add_monitor_bulk';
import { deleteMonitorBulk } from '../../routes/monitor_cruds/bulk_cruds/delete_monitor_bulk';
import { SyntheticsMonitorClient } from '../synthetics_monitor/synthetics_monitor_client';
import {
BrowserFields,
ConfigKey,
SyntheticsMonitorWithSecrets,
EncryptedSyntheticsMonitor,
ServiceLocationErrors,
ProjectBrowserMonitor,
ProjectMonitor,
Locations,
SyntheticsMonitor,
MonitorFields,
PrivateLocation,
} from '../../common/runtime_types';
} from '../../../common/runtime_types';
import {
syntheticsMonitorType,
syntheticsMonitor,
} from '../legacy_uptime/lib/saved_objects/synthetics_monitor';
import { normalizeProjectMonitor } from './normalizers/browser';
import { formatSecrets, normalizeSecrets } from './utils/secrets';
import { syncEditedMonitor } from '../routes/monitor_cruds/edit_monitor';
import { validateProjectMonitor } from '../routes/monitor_cruds/monitor_validation';
import type { UptimeServerSetup } from '../legacy_uptime/lib/adapters/framework';
} from '../../legacy_uptime/lib/saved_objects/synthetics_monitor';
import { UptimeServerSetup } from '../../legacy_uptime/lib/adapters';
import { formatSecrets, normalizeSecrets } from '../utils/secrets';
import { syncEditedMonitor } from '../../routes/monitor_cruds/edit_monitor';
import {
validateProjectMonitor,
validateMonitor,
ValidationResult,
} from '../../routes/monitor_cruds/monitor_validation';
import { normalizeProjectMonitor } from './normalizers';
interface StaleMonitor {
stale: boolean;
@ -44,7 +48,7 @@ interface StaleMonitor {
savedObjectId: string;
}
type StaleMonitorMap = Record<string, StaleMonitor>;
type FailedMonitors = Array<{ id?: string; reason: string; details: string; payload?: object }>;
type FailedError = Array<{ id?: string; reason: string; details: string; payload?: object }>;
export const INSUFFICIENT_FLEET_PERMISSIONS =
'Insufficient permissions. In order to configure private locations, you must have Fleet and Integrations write permissions. To resolve, please generate a new API key with a user who has Fleet and Integrations write permissions.';
@ -58,13 +62,13 @@ export class ProjectMonitorFormatter {
private savedObjectsClient: SavedObjectsClientContract;
private encryptedSavedObjectsClient: EncryptedSavedObjectsClient;
private staleMonitorsMap: StaleMonitorMap = {};
private monitors: ProjectBrowserMonitor[] = [];
private monitors: ProjectMonitor[] = [];
public createdMonitors: string[] = [];
public deletedMonitors: string[] = [];
public updatedMonitors: string[] = [];
public staleMonitors: string[] = [];
public failedMonitors: FailedMonitors = [];
public failedStaleMonitors: FailedMonitors = [];
public failedMonitors: FailedError = [];
public failedStaleMonitors: FailedError = [];
private server: UptimeServerSetup;
private projectFilter: string;
private syntheticsMonitorClient: SyntheticsMonitorClient;
@ -94,7 +98,7 @@ export class ProjectMonitorFormatter {
encryptedSavedObjectsClient: EncryptedSavedObjectsClient;
projectId: string;
spaceId: string;
monitors: ProjectBrowserMonitor[];
monitors: ProjectMonitor[];
server: UptimeServerSetup;
syntheticsMonitorClient: SyntheticsMonitorClient;
request: KibanaRequest;
@ -138,9 +142,9 @@ export class ProjectMonitorFormatter {
if (this.staleMonitorsMap[monitor.id]) {
this.staleMonitorsMap[monitor.id].stale = false;
}
normalizedUpdateMonitors.push(normM);
normalizedUpdateMonitors.push(normM as MonitorFields);
} else {
normalizedNewMonitors.push(normM);
normalizedNewMonitors.push(normM as MonitorFields);
}
}
}
@ -152,7 +156,7 @@ export class ProjectMonitorFormatter {
await this.handleStaleMonitors();
};
validatePermissions = async ({ monitor }: { monitor: ProjectBrowserMonitor }) => {
validatePermissions = async ({ monitor }: { monitor: ProjectMonitor }) => {
if (this.writeIntegrationPoliciesPermissions || (monitor.privateLocations ?? []).length === 0) {
return;
}
@ -167,11 +171,11 @@ export class ProjectMonitorFormatter {
}
};
validateProjectMonitor = async ({ monitor }: { monitor: ProjectBrowserMonitor }) => {
validateProjectMonitor = async ({ monitor }: { monitor: ProjectMonitor }) => {
try {
await this.validatePermissions({ monitor });
const normalizedMonitor = normalizeProjectMonitor({
const { normalizedFields: normalizedMonitor, unsupportedKeys } = normalizeProjectMonitor({
monitor,
locations: this.locations,
privateLocations: this.privateLocations,
@ -179,19 +183,40 @@ export class ProjectMonitorFormatter {
namespace: this.spaceId,
});
const validationResult = validateProjectMonitor(monitor, this.projectId);
if (!validationResult.valid) {
const { reason: message, details, payload } = validationResult;
if (unsupportedKeys.length) {
this.failedMonitors.push({
id: monitor.id,
reason: message,
details,
payload,
reason: 'Unsupported Heartbeat option',
details: `The following Heartbeat options are not supported for ${
monitor.type
} project monitors in ${this.server.kibanaVersion}: ${unsupportedKeys.join('|')}`,
});
if (this.staleMonitorsMap[monitor.id]) {
this.staleMonitorsMap[monitor.id].stale = false;
}
this.handleStreamingMessage({
message: `${monitor.id}: failed to create or update monitor`,
});
return null;
}
/* Validates that the payload sent from the synthetics agent is valid */
const { valid: isMonitorPayloadValid } = this.validateMonitor({
validationResult: validateProjectMonitor({
...monitor,
type: normalizedMonitor[ConfigKey.MONITOR_TYPE],
}),
monitorId: monitor.id,
});
if (!isMonitorPayloadValid) {
return null;
}
/* Validates that the normalized monitor is a valid monitor saved object type */
const { valid: isNormalizedMonitorValid } = this.validateMonitor({
validationResult: validateMonitor(normalizedMonitor as MonitorFields),
monitorId: monitor.id,
});
if (!isNormalizedMonitorValid) {
return null;
}
@ -310,7 +335,7 @@ export class ProjectMonitorFormatter {
try {
for (const monitor of monitors) {
const previousMonitor = await this.getExistingMonitor(monitor[ConfigKey.JOURNEY_ID]!);
await this.updateMonitor(previousMonitor, monitor);
await this.updateMonitor(previousMonitor, monitor as MonitorFields);
}
if (monitors.length > 0) {
@ -335,7 +360,7 @@ export class ProjectMonitorFormatter {
private updateMonitor = async (
previousMonitor: SavedObjectsFindResult<EncryptedSyntheticsMonitor>,
normalizedMonitor: BrowserFields
normalizedMonitor: MonitorFields
): Promise<{
editedMonitor: SavedObjectsUpdateResponse<EncryptedSyntheticsMonitor>;
errors: ServiceLocationErrors;
@ -478,4 +503,26 @@ export class ProjectMonitorFormatter {
this.subject?.next(message);
}
};
private validateMonitor = ({
validationResult,
monitorId,
}: {
validationResult: ValidationResult;
monitorId: string;
}) => {
const { reason: message, details, payload: validationPayload, valid } = validationResult;
if (!valid) {
this.failedMonitors.push({
id: monitorId,
reason: message,
details,
payload: validationPayload,
});
if (this.staleMonitorsMap[monitorId]) {
this.staleMonitorsMap[monitorId].stale = false;
}
}
return validationResult;
};
}

View file

@ -21,7 +21,7 @@ import { FtrProviderContext } from '../../../ftr_provider_context';
import { getFixtureJson } from './helper/get_fixture_json';
export default function ({ getService }: FtrProviderContext) {
describe('[POST] /internal/uptime/service/monitors', function () {
describe('AddNewMonitors', function () {
this.tags('skipCloud');
const supertestAPI = getService('supertest');

View file

@ -31,6 +31,9 @@ export default function ({ getService }: FtrProviderContext) {
const projectMonitorEndpoint = kibanaServerUrl + API_URLS.SYNTHETICS_MONITORS_PROJECT;
let projectMonitors: ProjectMonitorsRequest;
let httpProjectMonitors: ProjectMonitorsRequest;
let tcpProjectMonitors: ProjectMonitorsRequest;
let icmpProjectMonitors: ProjectMonitorsRequest;
let testPolicyId = '';
const testPrivateLocations = new PrivateLocationTestService(getService);
@ -88,6 +91,272 @@ export default function ({ getService }: FtrProviderContext) {
beforeEach(() => {
projectMonitors = setUniqueIds(getFixtureJson('project_browser_monitor'));
httpProjectMonitors = setUniqueIds(getFixtureJson('project_http_monitor'));
tcpProjectMonitors = setUniqueIds(getFixtureJson('project_tcp_monitor'));
icmpProjectMonitors = setUniqueIds(getFixtureJson('project_icmp_monitor'));
});
it('project monitors - handles http monitors', async () => {
const kibanaVersion = await kibanaServer.version.get();
const successfulMonitors = [httpProjectMonitors.monitors[1]];
try {
const messages = await parseStreamApiResponse(
projectMonitorEndpoint,
JSON.stringify(httpProjectMonitors)
);
expect(messages).to.have.length(3);
expect(messages[2].updatedMonitors).eql([]);
expect(messages[2].createdMonitors).eql(successfulMonitors.map((monitor) => monitor.id));
expect(messages[2].failedMonitors).eql([
{
id: httpProjectMonitors.monitors[0].id,
details: `The following Heartbeat options are not supported for ${httpProjectMonitors.monitors[0].type} project monitors in ${kibanaVersion}: check.response.body|unsupportedKey.nestedUnsupportedKey`,
reason: 'Unsupported Heartbeat option',
},
]);
for (const monitor of successfulMonitors) {
const journeyId = monitor.id;
const createdMonitorsResponse = await supertest
.get(API_URLS.SYNTHETICS_MONITORS)
.query({ filter: `${syntheticsMonitorType}.attributes.journey_id: ${journeyId}` })
.set('kbn-xsrf', 'true')
.expect(200);
expect(createdMonitorsResponse.body.monitors[0].attributes).to.eql({
__ui: {
is_tls_enabled: false,
},
'check.request.method': 'POST',
'check.response.status': ['200'],
config_id: '',
custom_heartbeat_id: `${journeyId}-test-suite-default`,
enabled: false,
form_monitor_type: 'http',
journey_id: journeyId,
locations: [
{
geo: {
lat: 0,
lon: 0,
},
id: 'localhost',
isInvalid: false,
isServiceManaged: true,
label: 'Local Synthetics Service',
status: 'experimental',
url: 'mockDevUrl',
},
],
max_redirects: '0',
name: monitor.name,
namespace: 'default',
origin: 'project',
original_space: 'default',
project_id: 'test-suite',
proxy_url: '',
'response.include_body': 'always',
'response.include_headers': false,
revision: 1,
schedule: {
number: '60',
unit: 'm',
},
'service.name': '',
'ssl.certificate': '',
'ssl.certificate_authorities': '',
'ssl.supported_protocols': ['TLSv1.1', 'TLSv1.2', 'TLSv1.3'],
'ssl.verification_mode': 'full',
tags: Array.isArray(monitor.tags) ? monitor.tags : monitor.tags?.split(','),
timeout: '80',
type: 'http',
urls: Array.isArray(monitor.urls) ? monitor.urls?.[0] : monitor.urls,
});
}
} finally {
await Promise.all([
successfulMonitors.map((monitor) => {
return deleteMonitor(monitor.id, httpProjectMonitors.project);
}),
]);
}
});
it('project monitors - handles tcp monitors', async () => {
const successfulMonitors = [tcpProjectMonitors.monitors[0], tcpProjectMonitors.monitors[1]];
const kibanaVersion = await kibanaServer.version.get();
try {
const messages = await parseStreamApiResponse(
projectMonitorEndpoint,
JSON.stringify(tcpProjectMonitors)
);
expect(messages).to.have.length(3);
expect(messages[2].updatedMonitors).eql([]);
expect(messages[2].createdMonitors).eql(successfulMonitors.map((monitor) => monitor.id));
expect(messages[2].failedMonitors).eql([
{
id: tcpProjectMonitors.monitors[2].id,
details: `The following Heartbeat options are not supported for ${tcpProjectMonitors.monitors[0].type} project monitors in ${kibanaVersion}: ports|unsupportedKey.nestedUnsupportedKey`,
reason: 'Unsupported Heartbeat option',
},
]);
for (const monitor of successfulMonitors) {
const journeyId = monitor.id;
const createdMonitorsResponse = await supertest
.get(API_URLS.SYNTHETICS_MONITORS)
.query({ filter: `${syntheticsMonitorType}.attributes.journey_id: ${journeyId}` })
.set('kbn-xsrf', 'true')
.expect(200);
expect(createdMonitorsResponse.body.monitors[0].attributes).to.eql({
__ui: {
is_tls_enabled: false,
},
config_id: '',
custom_heartbeat_id: `${journeyId}-test-suite-default`,
enabled: true,
form_monitor_type: 'tcp',
journey_id: journeyId,
locations: [
{
geo: {
lat: 0,
lon: 0,
},
id: 'localhost',
isInvalid: false,
isServiceManaged: true,
label: 'Local Synthetics Service',
status: 'experimental',
url: 'mockDevUrl',
},
],
name: monitor.name,
namespace: 'default',
origin: 'project',
original_space: 'default',
project_id: 'test-suite',
revision: 1,
schedule: {
number: '1',
unit: 'm',
},
proxy_url: '',
proxy_use_local_resolver: false,
'service.name': '',
'ssl.certificate': '',
'ssl.certificate_authorities': '',
'ssl.supported_protocols': ['TLSv1.1', 'TLSv1.2', 'TLSv1.3'],
'ssl.verification_mode': 'full',
tags: Array.isArray(monitor.tags) ? monitor.tags : monitor.tags?.split(','),
timeout: '16',
type: 'tcp',
hosts: Array.isArray(monitor.hosts) ? monitor.hosts?.[0] : monitor.hosts,
});
}
} finally {
await Promise.all([
successfulMonitors.map((monitor) => {
return deleteMonitor(monitor.id, tcpProjectMonitors.project);
}),
]);
}
});
it('project monitors - handles icmp monitors', async () => {
const successfulMonitors = [icmpProjectMonitors.monitors[0], icmpProjectMonitors.monitors[1]];
const kibanaVersion = await kibanaServer.version.get();
try {
const messages = await parseStreamApiResponse(
projectMonitorEndpoint,
JSON.stringify(icmpProjectMonitors)
);
expect(messages).to.have.length(3);
expect(messages[2].updatedMonitors).eql([]);
expect(messages[2].createdMonitors).eql(successfulMonitors.map((monitor) => monitor.id));
expect(messages[2].failedMonitors).eql([
{
id: icmpProjectMonitors.monitors[2].id,
details: `The following Heartbeat options are not supported for ${icmpProjectMonitors.monitors[0].type} project monitors in ${kibanaVersion}: unsupportedKey.nestedUnsupportedKey`,
reason: 'Unsupported Heartbeat option',
},
]);
for (const monitor of successfulMonitors) {
const journeyId = monitor.id;
const createdMonitorsResponse = await supertest
.get(API_URLS.SYNTHETICS_MONITORS)
.query({ filter: `${syntheticsMonitorType}.attributes.journey_id: ${journeyId}` })
.set('kbn-xsrf', 'true')
.expect(200);
expect(createdMonitorsResponse.body.monitors[0].attributes).to.eql({
config_id: '',
custom_heartbeat_id: `${journeyId}-test-suite-default`,
enabled: true,
form_monitor_type: 'icmp',
journey_id: journeyId,
locations: [
{
geo: {
lat: 0,
lon: 0,
},
id: 'localhost',
isInvalid: false,
isServiceManaged: true,
label: 'Local Synthetics Service',
status: 'experimental',
url: 'mockDevUrl',
},
{
agentPolicyId: testPolicyId,
concurrentMonitors: 1,
geo: {
lat: '',
lon: '',
},
id: testPolicyId,
isInvalid: false,
isServiceManaged: false,
label: 'Test private location 0',
},
],
name: monitor.name,
namespace: 'default',
origin: 'project',
original_space: 'default',
project_id: 'test-suite',
revision: 1,
schedule: {
number: '1',
unit: 'm',
},
'service.name': '',
tags: Array.isArray(monitor.tags) ? monitor.tags : monitor.tags?.split(','),
timeout: '16',
type: 'icmp',
hosts: Array.isArray(monitor.hosts) ? monitor.hosts?.[0] : monitor.hosts,
wait:
monitor.wait?.slice(-1) === 's'
? monitor.wait?.slice(0, -1)
: `${parseInt(monitor.wait?.slice(0, -1) || '1', 10) * 60}`,
});
}
} finally {
await Promise.all([
successfulMonitors.map((monitor) => {
return deleteMonitor(monitor.id, icmpProjectMonitors.project);
}),
]);
}
});
it('project monitors - returns a list of successfully created monitors', async () => {
@ -468,8 +737,7 @@ export default function ({ getService }: FtrProviderContext) {
expect(messages[0].updatedMonitors).eql([]);
expect(messages[0].failedMonitors).eql([
{
details:
'Invalid value "3m" supplied to "schedule" | Invalid value "" supplied to "tags"',
details: 'Invalid value "3m" supplied to "schedule"',
id: projectMonitors.monitors[0].id,
payload: {
content:
@ -492,6 +760,7 @@ export default function ({ getService }: FtrProviderContext) {
latency: 20,
upload: 3,
},
type: 'browser',
},
reason: 'Failed to save or update monitor. Configuration is not valid',
},
@ -813,9 +1082,9 @@ export default function ({ getService }: FtrProviderContext) {
privateLocations: ['Test private location 0'],
};
const testMonitors = [projectMonitors.monitors[0], secondMonitor];
const username = 'admin';
const username = 'test-username';
const roleName = 'uptime read only';
const password = `${username}-password`;
const password = `test-password`;
try {
await security.role.create(roleName, {
kibana: [
@ -1006,9 +1275,9 @@ export default function ({ getService }: FtrProviderContext) {
const packagePolicy = apiResponsePolicy.body.items.find(
(pkgPolicy: PackagePolicy) =>
pkgPolicy.id ===
monitorsResponse.body.monitors[0].attributes[ConfigKey.CUSTOM_HEARTBEAT_ID] +
'-' +
testPolicyId
`${
monitorsResponse.body.monitors[0].attributes[ConfigKey.CUSTOM_HEARTBEAT_ID]
}-${testPolicyId}`
);
expect(packagePolicy.name).eql(
`${projectMonitors.monitors[0].id}-${projectMonitors.project}-default-Test private location 0`
@ -1030,10 +1299,71 @@ export default function ({ getService }: FtrProviderContext) {
} finally {
await deleteMonitor(projectMonitors.monitors[0].id, projectMonitors.project);
const packagesResponse = await supertest.get(
'/api/fleet/package_policies?page=1&perPage=2000&kuery=ingest-package-policies.package.name%3A%20synthetics'
);
expect(packagesResponse.body.items.length).eql(0);
}
});
it('deletes integration policies for project monitors when private location is removed from the monitor - lightweight', async () => {
const monitorRequest = {
...httpProjectMonitors,
monitors: [
{ ...httpProjectMonitors.monitors[1], privateLocations: ['Test private location 0'] },
],
};
try {
await supertest
.put(API_URLS.SYNTHETICS_MONITORS_PROJECT)
.set('kbn-xsrf', 'true')
.send(monitorRequest);
const monitorsResponse = await supertest
.get(API_URLS.SYNTHETICS_MONITORS)
.query({
filter: `${syntheticsMonitorType}.attributes.journey_id: ${monitorRequest.monitors[0].id}`,
})
.set('kbn-xsrf', 'true')
.expect(200);
const apiResponsePolicy = await supertest.get(
'/api/fleet/package_policies?page=1&perPage=2000&kuery=ingest-package-policies.package.name%3A%20synthetics'
);
const packagePolicy = apiResponsePolicy.body.items.find(
(pkgPolicy: PackagePolicy) =>
pkgPolicy.id ===
`${
monitorsResponse.body.monitors[0].attributes[ConfigKey.CUSTOM_HEARTBEAT_ID]
}-${testPolicyId}`
);
expect(packagePolicy.policy_id).eql(testPolicyId);
await supertest
.put(API_URLS.SYNTHETICS_MONITORS_PROJECT)
.set('kbn-xsrf', 'true')
.send({
...monitorRequest,
monitors: [{ ...monitorRequest.monitors[0], privateLocations: [] }],
});
const apiResponsePolicy2 = await supertest.get(
'/api/fleet/package_policies?page=1&perPage=2000&kuery=ingest-package-policies.package.name%3A%20synthetics'
);
expect(apiResponsePolicy2.body.items.length).eql(0);
const packagePolicy2 = apiResponsePolicy2.body.items.find(
(pkgPolicy: PackagePolicy) =>
pkgPolicy.id ===
`${
monitorsResponse.body.monitors[0].attributes[ConfigKey.CUSTOM_HEARTBEAT_ID]
}-${testPolicyId}`
);
expect(packagePolicy2).eql(undefined);
} finally {
await deleteMonitor(projectMonitors.monitors[0].id, projectMonitors.project);
}
});
@ -1064,9 +1394,9 @@ export default function ({ getService }: FtrProviderContext) {
const packagePolicy = apiResponsePolicy.body.items.find(
(pkgPolicy: PackagePolicy) =>
pkgPolicy.id ===
monitorsResponse.body.monitors[0].attributes[ConfigKey.CUSTOM_HEARTBEAT_ID] +
'-' +
testPolicyId
`${
monitorsResponse.body.monitors[0].attributes[ConfigKey.CUSTOM_HEARTBEAT_ID]
}-${testPolicyId}`
);
expect(packagePolicy.policy_id).eql(testPolicyId);
@ -1099,9 +1429,9 @@ export default function ({ getService }: FtrProviderContext) {
const packagePolicy2 = apiResponsePolicy2.body.items.find(
(pkgPolicy: PackagePolicy) =>
pkgPolicy.id ===
monitorsResponse.body.monitors[0].attributes[ConfigKey.CUSTOM_HEARTBEAT_ID] +
'-' +
testPolicyId
`${
monitorsResponse.body.monitors[0].attributes[ConfigKey.CUSTOM_HEARTBEAT_ID]
}-${testPolicyId}`
);
expect(packagePolicy2).eql(undefined);
@ -1209,6 +1539,169 @@ export default function ({ getService }: FtrProviderContext) {
}
});
it('deletes integration policies when project monitors are deleted - lightweight', async () => {
const monitorRequest = {
...httpProjectMonitors,
monitors: [
{ ...httpProjectMonitors.monitors[1], privateLocations: ['Test private location 0'] },
],
};
try {
await supertest
.put(API_URLS.SYNTHETICS_MONITORS_PROJECT)
.set('kbn-xsrf', 'true')
.send(monitorRequest);
const monitorsResponse = await supertest
.get(API_URLS.SYNTHETICS_MONITORS)
.query({
filter: `${syntheticsMonitorType}.attributes.journey_id: ${monitorRequest.monitors[0].id}`,
})
.set('kbn-xsrf', 'true')
.expect(200);
const apiResponsePolicy = await supertest.get(
'/api/fleet/package_policies?page=1&perPage=2000&kuery=ingest-package-policies.package.name%3A%20synthetics'
);
const packagePolicy = apiResponsePolicy.body.items.find(
(pkgPolicy: PackagePolicy) =>
pkgPolicy.id ===
`${
monitorsResponse.body.monitors[0].attributes[ConfigKey.CUSTOM_HEARTBEAT_ID]
}-${testPolicyId}`
);
expect(packagePolicy.policy_id).eql(testPolicyId);
const configId = monitorsResponse.body.monitors[0].id;
const id = monitorsResponse.body.monitors[0].attributes[ConfigKey.CUSTOM_HEARTBEAT_ID];
const httpInput = packagePolicy.inputs.find(
(input: any) => input.type === 'synthetics/http'
);
expect(httpInput).to.eql({
type: 'synthetics/http',
policy_template: 'synthetics',
enabled: true,
streams: [
{
enabled: true,
data_stream: { type: 'synthetics', dataset: 'http' },
release: 'experimental',
vars: {
__ui: { value: '{"is_tls_enabled":false}', type: 'yaml' },
enabled: { value: false, type: 'bool' },
type: { value: 'http', type: 'text' },
name: { value: 'My Monitor 3', type: 'text' },
schedule: { value: '"@every 60m"', type: 'text' },
urls: { value: 'http://localhost:9200', type: 'text' },
'service.name': { value: '', type: 'text' },
timeout: { value: '80s', type: 'text' },
max_redirects: { value: '0', type: 'integer' },
proxy_url: { value: '', type: 'text' },
tags: { value: '["tag2","tag2"]', type: 'yaml' },
username: { value: '', type: 'text' },
password: { value: '', type: 'password' },
'response.include_headers': { value: false, type: 'bool' },
'response.include_body': { value: 'always', type: 'text' },
'check.request.method': { value: 'POST', type: 'text' },
'check.request.headers': {
value: '{"Content-Type":"application/x-www-form-urlencoded"}',
type: 'yaml',
},
'check.request.body': { value: null, type: 'yaml' },
'check.response.status': { value: '["200"]', type: 'yaml' },
'check.response.headers': { value: null, type: 'yaml' },
'check.response.body.positive': { value: '["Saved","saved"]', type: 'yaml' },
'check.response.body.negative': { value: null, type: 'yaml' },
'ssl.certificate_authorities': { value: null, type: 'yaml' },
'ssl.certificate': { value: null, type: 'yaml' },
'ssl.key': { value: null, type: 'yaml' },
'ssl.key_passphrase': { value: null, type: 'text' },
'ssl.verification_mode': { value: 'full', type: 'text' },
'ssl.supported_protocols': {
value: '["TLSv1.1","TLSv1.2","TLSv1.3"]',
type: 'yaml',
},
location_name: { value: 'Test private location 0', type: 'text' },
id: {
value: id,
type: 'text',
},
config_id: { value: configId, type: 'text' },
run_once: { value: false, type: 'bool' },
origin: { value: 'project', type: 'text' },
},
id: `synthetics/http-http-${id}-${testPolicyId}`,
compiled_stream: {
__ui: { is_tls_enabled: false },
type: 'http',
name: 'My Monitor 3',
id,
origin: 'project',
enabled: false,
urls: 'http://localhost:9200',
schedule: '@every 60m',
timeout: '80s',
max_redirects: 0,
tags: ['tag2', 'tag2'],
'response.include_headers': false,
'response.include_body': 'always',
'check.request.method': 'POST',
'check.request.headers': { 'Content-Type': 'application/x-www-form-urlencoded' },
'check.response.status': ['200'],
'check.response.body.positive': ['Saved', 'saved'],
'ssl.verification_mode': 'full',
'ssl.supported_protocols': ['TLSv1.1', 'TLSv1.2', 'TLSv1.3'],
processors: [
{ add_observer_metadata: { geo: { name: 'Test private location 0' } } },
{
add_fields: {
target: '',
fields: {
'monitor.fleet_managed': true,
config_id: configId,
},
},
},
],
},
},
],
});
await supertest
.put(API_URLS.SYNTHETICS_MONITORS_PROJECT)
.set('kbn-xsrf', 'true')
.send({
...monitorRequest,
monitors: [],
});
const apiResponsePolicy2 = await supertest.get(
'/api/fleet/package_policies?page=1&perPage=2000&kuery=ingest-package-policies.package.name%3A%20synthetics'
);
const packagePolicy2 = apiResponsePolicy2.body.items.find(
(pkgPolicy: PackagePolicy) =>
pkgPolicy.id ===
`${
monitorsResponse.body.monitors[0].attributes[ConfigKey.CUSTOM_HEARTBEAT_ID]
} - ${testPolicyId}`
);
expect(packagePolicy2).eql(undefined);
} finally {
await deleteMonitor(projectMonitors.monitors[0].id, projectMonitors.project);
const apiResponsePolicy2 = await supertest.get(
'/api/fleet/package_policies?page=1&perPage=2000&kuery=ingest-package-policies.package.name%3A%20synthetics'
);
expect(apiResponsePolicy2.body.items.length).eql(0);
}
});
it('handles updating package policies when project monitors are updated', async () => {
try {
await supertest

View file

@ -77,5 +77,6 @@
"namespace": "testnamespace",
"revision": 1,
"origin": "ui",
"form_monitor_type": "http"
"form_monitor_type": "http",
"journey_id": ""
}

View file

@ -1,6 +1,7 @@
{
"type": "tcp",
"locations": [],
"journey_id": "",
"enabled": true,
"schedule": {
"number": "3",

View file

@ -1,27 +1,27 @@
{
"keep_stale": true,
"project": "test-suite",
"monitors": [{
"throttling": {
"download": 5,
"upload": 3,
"latency": 20
},
"schedule": 10,
"locations": [
"localhost"
],
"params": {},
"playwrightOptions": {
"headless": true,
"chromiumSandbox": false
},
"name": "check if title is present",
"id": "check-if-title-is-present",
"tags": [],
"content": "UEsDBBQACAAIAON5qVQAAAAAAAAAAAAAAAAfAAAAZXhhbXBsZXMvdG9kb3MvYmFzaWMuam91cm5leS50c22Q0WrDMAxF3/sVF7MHB0LMXlc6RvcN+wDPVWNviW0sdUsp/fe5SSiD7UFCWFfHujIGlpnkybwxFTZfoY/E3hsaLEtwhs9RPNWKDU12zAOxkXRIbN4tB9d9pFOJdO6EN2HMqQguWN9asFBuQVMmJ7jiWNII9fIXrbabdUYr58l9IhwhQQZCYORCTFFUC31Btj21NRc7Mq4Nds+4bDD/pNVgT9F52Jyr2Fa+g75LAPttg8yErk+S9ELpTmVotlVwnfNCuh2lepl3+JflUmSBJ3uggt1v9INW/lHNLKze9dJe1J3QJK8pSvWkm6aTtCet5puq+x63+AFQSwcIAPQ3VfcAAACcAQAAUEsBAi0DFAAIAAgA43mpVAD0N1X3AAAAnAEAAB8AAAAAAAAAAAAgAKSBAAAAAGV4YW1wbGVzL3RvZG9zL2Jhc2ljLmpvdXJuZXkudHNQSwUGAAAAAAEAAQBNAAAARAEAAAAA",
"filter": {
"match": "check if title is present"
}
}]
"keep_stale": true,
"project": "test-suite",
"monitors": [{
"throttling": {
"download": 5,
"upload": 3,
"latency": 20
},
"schedule": 10,
"locations": [
"localhost"
],
"params": {},
"playwrightOptions": {
"headless": true,
"chromiumSandbox": false
},
"name": "check if title is present",
"id": "check-if-title-is-present",
"tags": [],
"content": "UEsDBBQACAAIAON5qVQAAAAAAAAAAAAAAAAfAAAAZXhhbXBsZXMvdG9kb3MvYmFzaWMuam91cm5leS50c22Q0WrDMAxF3/sVF7MHB0LMXlc6RvcN+wDPVWNviW0sdUsp/fe5SSiD7UFCWFfHujIGlpnkybwxFTZfoY/E3hsaLEtwhs9RPNWKDU12zAOxkXRIbN4tB9d9pFOJdO6EN2HMqQguWN9asFBuQVMmJ7jiWNII9fIXrbabdUYr58l9IhwhQQZCYORCTFFUC31Btj21NRc7Mq4Nds+4bDD/pNVgT9F52Jyr2Fa+g75LAPttg8yErk+S9ELpTmVotlVwnfNCuh2lepl3+JflUmSBJ3uggt1v9INW/lHNLKze9dJe1J3QJK8pSvWkm6aTtCet5puq+x63+AFQSwcIAPQ3VfcAAACcAQAAUEsBAi0DFAAIAAgA43mpVAD0N1X3AAAAnAEAAB8AAAAAAAAAAAAgAKSBAAAAAGV4YW1wbGVzL3RvZG9zL2Jhc2ljLmpvdXJuZXkudHNQSwUGAAAAAAEAAQBNAAAARAEAAAAA",
"filter": {
"match": "check if title is present"
}
}]
}

View file

@ -0,0 +1,76 @@
{
"project": "test-suite",
"keep_stale": false,
"monitors": [
{
"locations": ["localhost"],
"type": "http",
"enabled": false,
"id": "my-monitor-2",
"name": "My Monitor 2",
"urls": [
"http://localhost:9200"
],
"schedule": 60,
"timeout": "80s",
"check.request": {
"method": "POST",
"headers": {
"Content-Type": "application/x-www-form-urlencoded"
}
},
"response": {
"include_body": "always"
},
"response.include_headers": false,
"check.response": {
"status": [
200
],
"body": [
"Saved",
"saved"
]
},
"content": "",
"unsupportedKey": {
"nestedUnsupportedKey": "unsupportedValue"
}
},
{
"locations": ["localhost"],
"type": "http",
"enabled": false,
"id": "my-monitor-3",
"name": "My Monitor 3",
"urls": [
"http://localhost:9200"
],
"schedule": 60,
"timeout": "80s",
"check.request": {
"method": "POST",
"headers": {
"Content-Type": "application/x-www-form-urlencoded"
}
},
"response": {
"include_body": "always"
},
"tags": "tag2,tag2",
"response.include_headers": false,
"check.response": {
"status": [
200
],
"body":{
"positive": [
"Saved",
"saved"
]
}
},
"content": ""
}
]
}

View file

@ -0,0 +1,47 @@
{
"project": "test-suite",
"keep_stale": true,
"monitors": [
{
"locations": [ "localhost" ],
"type": "icmp",
"id": "Cloudflare-DNS",
"name": "Cloudflare DNS",
"hosts": [ "1.1.1.1" ],
"schedule": 1,
"tags": [ "service:smtp", "org:google" ],
"privateLocations": [ "Test private location 0" ],
"content": "",
"wait": "30s"
},
{
"locations": [ "localhost" ],
"type": "icmp",
"id": "Cloudflare-DNS-2",
"name": "Cloudflare DNS 2",
"hosts": "1.1.1.1",
"schedule": 1,
"tags": "tag1,tag2",
"privateLocations": [ "Test private location 0" ],
"content": "",
"wait": "1m"
},
{
"locations": [ "localhost" ],
"type": "icmp",
"id": "Cloudflare-DNS-3",
"name": "Cloudflare DNS 3",
"hosts": "1.1.1.1",
"schedule": 1,
"tags": "tag1,tag2",
"privateLocations": [ "Test private location 0" ],
"content": "",
"unsupportedKey": {
"nestedUnsupportedKey": "unnsuportedValue"
}
}
]
}

View file

@ -0,0 +1,43 @@
{
"project": "test-suite",
"keep_stale": true,
"monitors": [
{
"locations": [ "localhost" ],
"type": "tcp",
"id": "gmail-smtp",
"name": "GMail SMTP",
"hosts": [ "smtp.gmail.com:587" ],
"schedule": 1,
"tags": [ "service:smtp", "org:google" ],
"privateLocations": [ "BEEP" ],
"content": ""
},
{
"locations": [ "localhost" ],
"type": "tcp",
"id": "always-down",
"name": "Always Down",
"hosts": "localhost:18278",
"schedule": 1,
"tags": "tag1,tag2",
"privateLocations": [ "BEEP" ],
"content": ""
},
{
"locations": [ "localhost" ],
"type": "tcp",
"id": "always-down",
"name": "Always Down",
"hosts": "localhost",
"ports": ["5698"],
"schedule": 1,
"tags": "tag1,tag2",
"privateLocations": [ "BEEP" ],
"content": "",
"unsupportedKey": {
"nestedUnsupportedKey": "unnsuportedValue"
}
}
]
}

View file

@ -13,7 +13,7 @@ import { FtrProviderContext } from '../../../ftr_provider_context';
import { getFixtureJson } from './helper/get_fixture_json';
export default function ({ getService }: FtrProviderContext) {
describe('[GET] /internal/uptime/service/monitors', function () {
describe('getSyntheticsMonitors', function () {
this.tags('skipCloud');
const supertest = getService('supertest');