[Synthetics] Multi space monitors !! (#221568)

## Summary

Multi space monitors !!

Fixes https://github.com/elastic/kibana/issues/164294

User will be able to choose in which space monitors will be available !!

<img width="1728" alt="image"
src="https://github.com/user-attachments/assets/f01ac226-ed54-4e96-b6f4-27f0134a9be5"
/>


### Technical 
This is being done by registering another saved object type and for
existing monitors it will continue to work as right now but for newly
created monitors user will have ability to specify spaces or choose
multiple spaces or all.

### Testing

1. Create few monitors before this PR in multiple spaces
2. Create multiple monitors in multiple spaces after this PR
3. Make sure filtering, editing and deleting, creating works as expected
on both set of monitors

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Shahzad 2025-06-25 10:47:47 +02:00 committed by GitHub
parent b2d91b43f3
commit f317cec25b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
139 changed files with 3470 additions and 1275 deletions

View file

@ -1159,6 +1159,34 @@
"type",
"urls"
],
"synthetics-monitor-multi-space": [
"alert",
"alert.status",
"alert.status.enabled",
"alert.tls",
"alert.tls.enabled",
"config_id",
"custom_heartbeat_id",
"enabled",
"hash",
"hosts",
"id",
"journey_id",
"locations",
"locations.id",
"locations.label",
"maintenance_windows",
"name",
"origin",
"project_id",
"schedule",
"schedule.number",
"tags",
"throttling",
"throttling.label",
"type",
"urls"
],
"synthetics-param": [],
"synthetics-private-location": [],
"synthetics-privates-locations": [],

View file

@ -3832,6 +3832,136 @@
}
}
},
"synthetics-monitor-multi-space": {
"dynamic": false,
"properties": {
"alert": {
"properties": {
"status": {
"properties": {
"enabled": {
"type": "boolean"
}
}
},
"tls": {
"properties": {
"enabled": {
"type": "boolean"
}
}
}
}
},
"config_id": {
"type": "keyword"
},
"custom_heartbeat_id": {
"type": "keyword"
},
"enabled": {
"type": "boolean"
},
"hash": {
"type": "keyword"
},
"hosts": {
"fields": {
"keyword": {
"ignore_above": 256,
"type": "keyword"
}
},
"type": "text"
},
"id": {
"type": "keyword"
},
"journey_id": {
"type": "keyword"
},
"locations": {
"properties": {
"id": {
"fields": {
"text": {
"type": "text"
}
},
"ignore_above": 256,
"type": "keyword"
},
"label": {
"type": "text"
}
}
},
"maintenance_windows": {
"type": "keyword"
},
"name": {
"fields": {
"keyword": {
"ignore_above": 256,
"normalizer": "lowercase",
"type": "keyword"
}
},
"type": "text"
},
"origin": {
"type": "keyword"
},
"project_id": {
"fields": {
"text": {
"type": "text"
}
},
"type": "keyword"
},
"schedule": {
"properties": {
"number": {
"type": "integer"
}
}
},
"tags": {
"fields": {
"text": {
"type": "text"
}
},
"type": "keyword"
},
"throttling": {
"properties": {
"label": {
"type": "keyword"
}
}
},
"type": {
"fields": {
"keyword": {
"ignore_above": 256,
"type": "keyword"
}
},
"type": "text"
},
"urls": {
"fields": {
"keyword": {
"ignore_above": 256,
"type": "keyword"
}
},
"type": "text"
}
}
},
"synthetics-param": {
"dynamic": false,
"properties": {}

View file

@ -11,4 +11,4 @@ export { registerCoreObjectTypes } from './registration';
// set minimum number of registered saved objects to ensure no object types are removed after 8.8
// declared in internal implementation explicitly to prevent unintended changes.
export const SAVED_OBJECT_TYPES_COUNT = 135 as const;
export const SAVED_OBJECT_TYPES_COUNT = 136 as const;

View file

@ -177,6 +177,7 @@ describe('checking migration metadata changes on all registered SO types', () =>
"spaces-usage-stats": "084bd0f080f94fb5735d7f3cf12f13ec92f36bad",
"synthetics-dynamic-settings": "7804b079cc502f16526f7c9491d1397cc1ec67db",
"synthetics-monitor": "fdebfa2449d2b934972d1743dc78c34ae9ebc9c1",
"synthetics-monitor-multi-space": "c8c9dab447ba8a7383041f55ba80757365d114c5",
"synthetics-param": "9776c9b571d35f0d0397e8915e035ea1dc026db7",
"synthetics-private-location": "27aaa44f792f70b734905e44e3e9b56bbeac7b86",
"synthetics-privates-locations": "36036b881524108c7327fe14bd224c6e4d972cb5",

View file

@ -149,6 +149,7 @@ const previouslyRegisteredTypes = [
'space',
'spaces-usage-stats',
'synthetics-monitor',
'synthetics-monitor-multi-space',
'synthetics-param',
'synthetics-privates-locations',
'synthetics-private-location',

View file

@ -107,6 +107,7 @@ const STANDARD_LIST_TYPES = [
'cases-connector-mappings',
// synthetics based objects
'synthetics-monitor',
'synthetics-monitor-multi-space',
'uptime-dynamic-settings',
'synthetics-privates-locations',
'synthetics-private-location',

View file

@ -45848,7 +45848,6 @@
"xpack.synthetics.syntheticsEmbeddable.monitors.ariaLabel": "Aperçu des moniteurs",
"xpack.synthetics.syntheticsEmbeddable.stats.ariaLabel": "Statistiques des moniteurs",
"xpack.synthetics.syntheticsFeatureCatalogueTitle": "Synthetics",
"xpack.synthetics.syntheticsMonitors.label": "Synthetics - Moniteur",
"xpack.synthetics.tableTitle.showing": "Affichage de {count} sur {total} {label}",
"xpack.synthetics.tagsSelectPlaceholder": "Sélectionner des balises",
"xpack.synthetics.testDetails.after": "Après",

View file

@ -45899,7 +45899,6 @@
"xpack.synthetics.syntheticsEmbeddable.monitors.ariaLabel": "モニター概要",
"xpack.synthetics.syntheticsEmbeddable.stats.ariaLabel": "モニター統計",
"xpack.synthetics.syntheticsFeatureCatalogueTitle": "Synthetics",
"xpack.synthetics.syntheticsMonitors.label": "Synthetics - 監視",
"xpack.synthetics.tableTitle.showing": "{count}/{total} {label}を表示中",
"xpack.synthetics.tagsSelectPlaceholder": "タグを選択",
"xpack.synthetics.testDetails.after": "後",

View file

@ -45875,7 +45875,6 @@
"xpack.synthetics.syntheticsEmbeddable.monitors.ariaLabel": "监测概览",
"xpack.synthetics.syntheticsEmbeddable.stats.ariaLabel": "监测统计信息",
"xpack.synthetics.syntheticsFeatureCatalogueTitle": "Synthetics",
"xpack.synthetics.syntheticsMonitors.label": "Synthetics - 监测",
"xpack.synthetics.tableTitle.showing": "正在显示 {count} 个(共 {total} 个){label}",
"xpack.synthetics.tagsSelectPlaceholder": "选择标签",
"xpack.synthetics.testDetails.after": "之后",

View file

@ -52,7 +52,7 @@ const TagsList = ({
const tagsToDisplay = tags.slice(0, toDisplay);
return (
<EuiFlexGroup wrap gutterSize="m" css={{ maxWidth: 400 }} alignItems="baseline">
<EuiFlexGroup wrap gutterSize="xs" css={{ maxWidth: 400 }} alignItems="baseline">
{tagsToDisplay.map((tag) => (
// filtering only makes sense in monitor list, where we have summary
<EuiFlexItem key={tag} grow={false}>

View file

@ -155,6 +155,7 @@ export const DEFAULT_COMMON_FIELDS: CommonFields = {
[ConfigKey.LABELS]: {},
[ConfigKey.MAX_ATTEMPTS]: 2,
[ConfigKey.MAINTENANCE_WINDOWS]: [],
[ConfigKey.KIBANA_SPACES]: [],
revision: 1,
};

View file

@ -79,6 +79,7 @@ export enum ConfigKey {
MONITOR_QUERY_ID = 'id',
MAX_ATTEMPTS = 'max_attempts',
MAINTENANCE_WINDOWS = 'maintenance_windows',
KIBANA_SPACES = 'spaces',
}
export const secretKeys = [

View file

@ -88,6 +88,7 @@ export const CommonFieldsCodec = t.intersection([
[ConfigKey.PARAMS]: t.string,
[ConfigKey.LABELS]: t.record(t.string, t.string),
[ConfigKey.MAINTENANCE_WINDOWS]: t.array(t.string),
[ConfigKey.KIBANA_SPACES]: t.array(t.string),
retest_on_failure: t.boolean,
}),
]);
@ -357,7 +358,7 @@ const HeartbeatFieldsCodec = t.intersection([
'monitor.id': t.string,
'monitor.project.id': t.string,
'monitor.fleet_managed': t.boolean,
meta: t.record(t.string, t.string),
meta: t.record(t.string, t.union([t.string, t.array(t.string)])),
}),
]);

View file

@ -52,7 +52,7 @@ export const OverviewStatusMetaDataCodec = t.intersection([
projectId: t.string,
updated_at: t.string,
timestamp: t.string,
spaceId: t.string,
spaces: t.array(t.string),
urls: t.string,
maintenanceWindows: t.array(t.string),
}),

View file

@ -5,7 +5,15 @@
* 2.0.
*/
export const syntheticsMonitorType = 'synthetics-monitor';
export const monitorAttributes = `${syntheticsMonitorType}.attributes`;
export const legacySyntheticsMonitorTypeSingle = 'synthetics-monitor';
export const legacyMonitorAttributes = `${legacySyntheticsMonitorTypeSingle}.attributes`;
export const syntheticsMonitorSavedObjectType = 'synthetics-monitor-multi-space';
export const syntheticsMonitorAttributes = `${syntheticsMonitorSavedObjectType}.attributes`;
export const syntheticsParamType = 'synthetics-param';
export const syntheticsMonitorSOTypes = [
syntheticsMonitorSavedObjectType,
legacySyntheticsMonitorTypeSingle,
];

View file

@ -0,0 +1,64 @@
/*
* 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 moment, { Moment } from 'moment';
export const SHORT_TS_LOCALE = 'en-short-locale';
export const SHORT_TIMESPAN_LOCALE = {
relativeTime: {
future: 'in %s',
past: '%s ago',
s: '%ds',
ss: '%ss',
m: '%dm',
mm: '%dm',
h: '%dh',
hh: '%dh',
d: '%dd',
dd: '%dd',
M: '%d Mon',
MM: '%d Mon',
y: '%d Yr',
yy: '%d Yr',
},
};
export const parseTimestamp = (tsValue: string): Moment => {
let parsed = Date.parse(tsValue);
if (isNaN(parsed)) {
parsed = parseInt(tsValue, 10);
}
return moment(parsed);
};
export const getShortTimeStamp = (timeStamp: moment.Moment, relative = false) => {
if (relative) {
const prevLocale: string = moment.locale() ?? 'en';
const shortLocale = moment.locale(SHORT_TS_LOCALE) === SHORT_TS_LOCALE;
if (!shortLocale) {
moment.defineLocale(SHORT_TS_LOCALE, SHORT_TIMESPAN_LOCALE);
}
let shortTimestamp;
if (typeof timeStamp === 'string') {
shortTimestamp = parseTimestamp(timeStamp).fromNow();
} else {
shortTimestamp = timeStamp.fromNow();
}
// Reset it so, it doesn't impact other part of the app
moment.locale(prevLocale);
return shortTimestamp;
} else {
if (moment().diff(timeStamp, 'd') >= 1) {
return timeStamp.format('ll LTS');
}
return timeStamp.format('LTS');
}
};

View file

@ -62,14 +62,14 @@ journey('ProjectMonitorReadOnly', async ({ page, params }) => {
// hash is always reset to empty string when monitor is edited
// this ensures that when the monitor is pushed again, the monitor
// config in the process takes precedence
expect(omit(newConfiguration, ['updated_at'])).toEqual(
expect(omit(newConfiguration, ['updated_at', 'created_at'])).toEqual(
omit(
{
...originalMonitorConfiguration,
hash: '',
revision: 2,
},
['updated_at']
['updated_at', 'created_at']
)
);
});
@ -88,7 +88,7 @@ journey('ProjectMonitorReadOnly', async ({ page, params }) => {
// hash is always reset to empty string when monitor is edited
// this ensures that when the monitor is pushed again, the monitor
// config in the process takes precedence
expect(omit(newConfiguration, ['updated_at'])).toEqual(
expect(omit(newConfiguration, ['updated_at', 'created_at'])).toEqual(
omit(
{
...originalMonitorConfiguration,
@ -104,7 +104,7 @@ journey('ProjectMonitorReadOnly', async ({ page, params }) => {
},
enabled: !originalMonitorConfiguration?.enabled,
},
['updated_at']
['updated_at', 'created_at']
)
);
});
@ -112,13 +112,13 @@ journey('ProjectMonitorReadOnly', async ({ page, params }) => {
step('Monitor can be re-pushed and overwrite any changes', async () => {
await addTestMonitorProject(params.kibanaUrl, monitorName);
const repushedConfiguration = await services.getMonitor(monitorId);
expect(omit(repushedConfiguration, ['updated_at'])).toEqual(
expect(omit(repushedConfiguration, ['updated_at', 'created_at'])).toEqual(
omit(
{
...originalMonitorConfiguration,
revision: 4,
},
['updated_at']
['updated_at', 'created_at']
)
);
});

View file

@ -48,7 +48,9 @@ export const cleanTestMonitors = async (params: Record<string, any>) => {
const server = getService('kibanaServer');
try {
await server.savedObjects.clean({ types: ['synthetics-monitor'] });
await server.savedObjects.clean({
types: ['synthetics-monitor', 'synthetics-monitor-multi-space'],
});
} catch (e) {
// eslint-disable-next-line no-console
console.log(e);

View file

@ -202,7 +202,9 @@ export class SyntheticsServices {
const getService = this.params.getService;
const server = getService('kibanaServer');
await server.savedObjects.clean({ types: ['synthetics-monitor', 'alert'] });
await server.savedObjects.clean({
types: ['synthetics-monitor', 'synthetics-monitor-multi-space', 'alert'],
});
await this.cleanUpAlerts();
} catch (e) {
// eslint-disable-next-line no-console

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 { getUpdatedSpacesSelection } from './monitor_spaces';
const ALL_SPACES_ID = 'all-spaces-id';
const CURRENT_SPACE_ID = 'current-space-id';
describe('getUpdatedSpacesSelection', () => {
it('returns only allSpacesId if allSpacesId is selected', () => {
expect(
getUpdatedSpacesSelection(['foo', ALL_SPACES_ID, 'bar'], CURRENT_SPACE_ID, ALL_SPACES_ID)
).toEqual([ALL_SPACES_ID]);
});
it('returns currentSpaceId if nothing is selected', () => {
expect(getUpdatedSpacesSelection([], CURRENT_SPACE_ID, ALL_SPACES_ID)).toEqual([
CURRENT_SPACE_ID,
]);
});
it('adds currentSpaceId if not present', () => {
expect(getUpdatedSpacesSelection(['foo', 'bar'], CURRENT_SPACE_ID, ALL_SPACES_ID)).toEqual([
'foo',
'bar',
CURRENT_SPACE_ID,
]);
});
it('returns selectedIds if currentSpaceId is already present', () => {
expect(
getUpdatedSpacesSelection(['foo', CURRENT_SPACE_ID, 'bar'], CURRENT_SPACE_ID, ALL_SPACES_ID)
).toEqual(['foo', CURRENT_SPACE_ID, 'bar']);
});
it('returns selectedIds if no currentSpaceId is provided', () => {
expect(getUpdatedSpacesSelection(['foo', 'bar'], undefined, ALL_SPACES_ID)).toEqual([
'foo',
'bar',
]);
});
it('returns only allSpacesId if allSpacesId is the only selection', () => {
expect(getUpdatedSpacesSelection([ALL_SPACES_ID], CURRENT_SPACE_ID, ALL_SPACES_ID)).toEqual([
ALL_SPACES_ID,
]);
});
it('returns empty array if nothing is selected and no currentSpaceId', () => {
expect(getUpdatedSpacesSelection([], undefined, ALL_SPACES_ID)).toEqual([]);
});
});

View file

@ -0,0 +1,135 @@
/*
* 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 { useFormContext } from 'react-hook-form';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import React, { useEffect } from 'react';
import { EuiComboBox } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { ALL_SPACES_ID } from '@kbn/security-plugin/public';
import { useKibanaSpace } from '../../../../../hooks/use_kibana_space';
import { ClientPluginsStart } from '../../../../../plugin';
import { ConfigKey } from '../constants';
export interface MonitorSpacesProps {
onChange: (value: string[]) => void;
value?: string[] | null;
readOnly?: boolean;
}
/**
* Returns the updated list of selected space ids based on the selection logic.
* @param selectedIds Array of selected space ids
* @param currentSpaceId The id of the current space
* @param allSpacesId The id representing "All spaces"
*/
export function getUpdatedSpacesSelection(
selectedIds: string[],
currentSpaceId?: string,
allSpacesId?: string
): string[] {
if (allSpacesId && selectedIds.includes(allSpacesId)) {
// Only return allSpacesId if selected, ignore all others including currentSpaceId
return [allSpacesId];
}
// Remove allSpacesId if present (should only be present alone)
const filtered = allSpacesId ? selectedIds.filter((id) => id !== allSpacesId) : selectedIds;
if (filtered.length === 0 && currentSpaceId) {
return [currentSpaceId];
}
if (currentSpaceId && !filtered.includes(currentSpaceId)) {
return [...filtered, currentSpaceId];
}
return filtered;
}
export const MonitorSpaces = ({ value, onChange, ...rest }: MonitorSpacesProps) => {
const { space: currentSpace } = useKibanaSpace();
const { services } = useKibana<ClientPluginsStart>();
const [spacesList, setSpacesList] = React.useState<Array<{ id: string; label: string }>>([]);
const data = services.spaces?.ui.useSpaces();
const {
control,
formState: { isSubmitted },
trigger,
} = useFormContext();
const { isTouched, error } = control.getFieldState(ConfigKey.KIBANA_SPACES);
const showFieldInvalid = (isSubmitted || isTouched) && !!error;
useEffect(() => {
if (data?.spacesDataPromise) {
data.spacesDataPromise.then((spacesData) => {
setSpacesList([
allSpacesOption,
...[...spacesData.spacesMap].map(([spaceId, dataS]) => ({
id: spaceId,
label: dataS.name,
})),
]);
});
}
}, [data]);
// Ensure selected options always include the current space
const selectedIds = React.useMemo(() => {
if (!currentSpace) {
return value ?? [];
}
if (!value || value.length === 0) {
return [currentSpace.id];
}
if (value.includes(ALL_SPACES_ID)) {
// If "All spaces" is selected, return it alone
return [ALL_SPACES_ID];
}
return value.includes(currentSpace.id) ? value : [...value, currentSpace.id];
}, [value, currentSpace]);
// Compute if "All spaces" is selected
const isAllSpacesSelected = selectedIds.includes(ALL_SPACES_ID);
return (
<EuiComboBox<string>
fullWidth
aria-label={SPACES_LABEL}
placeholder={SPACES_LABEL}
isInvalid={showFieldInvalid}
onBlur={async () => {
await trigger();
}}
options={spacesList.map((option) =>
isAllSpacesSelected && option.id !== ALL_SPACES_ID ? { ...option, disabled: true } : option
)}
selectedOptions={spacesList.filter(({ id }) => selectedIds.includes(id))}
isClearable={true}
onChange={(selected) => {
const newSelectedIds = selected.map((option) => option.id!);
const updatedIds = getUpdatedSpacesSelection(
newSelectedIds,
currentSpace?.id,
allSpacesOption.id
);
onChange(updatedIds);
}}
/>
);
};
export const ALL_SPACES_LABEL = i18n.translate('xpack.synthetics.spaceList.allSpacesLabel', {
defaultMessage: `* All spaces`,
});
const allSpacesOption = {
id: ALL_SPACES_ID,
label: ALL_SPACES_LABEL,
};
const SPACES_LABEL = i18n.translate('xpack.synthetics.privateLocation.spacesLabel', {
defaultMessage: 'Spaces ',
});

View file

@ -32,6 +32,7 @@ import {
} from '@elastic/eui';
import { MaintenanceWindowsLink } from '../fields/maintenance_windows/create_maintenance_windows_btn';
import { MaintenanceWindowsFieldProps } from '../fields/maintenance_windows/maintenance_windows';
import { MonitorSpacesProps } from '../fields/monitor_spaces';
import { kibanaService } from '../../../../../utils/kibana_service';
import {
PROFILE_OPTIONS,
@ -63,6 +64,7 @@ import {
TextArea,
ThrottlingWrapper,
MaintenanceWindowsFieldWrapper,
KibanaSpacesWrapper,
} from './field_wrappers';
import { useMonitorName } from '../../../hooks/use_monitor_name';
import {
@ -1701,4 +1703,24 @@ export const FIELD = (readOnly?: boolean): FieldMap => ({
}),
labelAppend: <MaintenanceWindowsLink />,
},
[ConfigKey.KIBANA_SPACES]: {
fieldKey: ConfigKey.KIBANA_SPACES,
component: KibanaSpacesWrapper,
label: i18n.translate('xpack.synthetics.monitorConfig.kibanaSpaces.label', {
defaultMessage: 'Kibana spaces',
}),
helpText: i18n.translate('xpack.synthetics.monitorConfig.kibanaSpaces.helpText', {
defaultMessage:
' Current space should always be part of list, unless All spaces is selected.',
}),
controlled: true,
props: ({ field, setValue, trigger }): MonitorSpacesProps => ({
readOnly,
value: field?.value || [],
onChange: async (spaces?: string[]) => {
setValue(ConfigKey.KIBANA_SPACES, spaces);
await trigger(ConfigKey.KIBANA_SPACES);
},
}),
},
});

View file

@ -28,6 +28,7 @@ import {
EuiTextArea,
EuiTextAreaProps,
} from '@elastic/eui';
import { MonitorSpaces, MonitorSpacesProps } from '../fields/monitor_spaces';
import {
MaintenanceWindowsField,
MaintenanceWindowsFieldProps,
@ -163,3 +164,7 @@ export const MaintenanceWindowsFieldWrapper = React.forwardRef<
unknown,
MaintenanceWindowsFieldProps
>((props, _ref) => <MaintenanceWindowsField {...props} />);
export const KibanaSpacesWrapper = React.forwardRef<unknown, MonitorSpacesProps>((props, _ref) => (
<MonitorSpaces {...props} />
));

View file

@ -203,6 +203,17 @@ const TLS_OPTIONS = (readOnly: boolean): AdvancedFieldGroup => ({
],
});
const KIBANA_SPACES_OPTIONS = (readOnly: boolean): AdvancedFieldGroup => ({
title: i18n.translate('xpack.synthetics.monitorConfig.section.kibanaSpaces.title', {
defaultMessage: 'Kibana Spaces',
}),
description: i18n.translate('xpack.synthetics.monitorConfig.kibanaSpaces.description', {
defaultMessage:
'Select the Kibana spaces where this monitor should be available. Current space should always be part of list, unless All spaces is selected.',
}),
components: [FIELD(readOnly)[ConfigKey.KIBANA_SPACES]],
});
export const FORM_CONFIG = (readOnly: boolean): FieldConfig => ({
[FormMonitorType.HTTP]: {
step1: [FIELD(readOnly)[ConfigKey.FORM_MONITOR_TYPE]],
@ -225,6 +236,7 @@ export const FORM_CONFIG = (readOnly: boolean): FieldConfig => ({
HTTP_ADVANCED(readOnly).responseConfig,
HTTP_ADVANCED(readOnly).responseChecks,
TLS_OPTIONS(readOnly),
KIBANA_SPACES_OPTIONS(readOnly),
],
},
[FormMonitorType.TCP]: {
@ -246,6 +258,7 @@ export const FORM_CONFIG = (readOnly: boolean): FieldConfig => ({
TCP_ADVANCED(readOnly).requestConfig,
TCP_ADVANCED(readOnly).responseChecks,
TLS_OPTIONS(readOnly),
KIBANA_SPACES_OPTIONS(readOnly),
],
},
[FormMonitorType.MULTISTEP]: {
@ -273,6 +286,7 @@ export const FORM_CONFIG = (readOnly: boolean): FieldConfig => ({
},
MAINTENANCE_WINDOWS_OPTIONS(readOnly),
...BROWSER_ADVANCED(readOnly),
KIBANA_SPACES_OPTIONS(readOnly),
],
},
[FormMonitorType.SINGLE]: {
@ -300,6 +314,7 @@ export const FORM_CONFIG = (readOnly: boolean): FieldConfig => ({
},
MAINTENANCE_WINDOWS_OPTIONS(readOnly),
...BROWSER_ADVANCED(readOnly),
KIBANA_SPACES_OPTIONS(readOnly),
],
},
[FormMonitorType.ICMP]: {
@ -319,6 +334,7 @@ export const FORM_CONFIG = (readOnly: boolean): FieldConfig => ({
DEFAULT_DATA_OPTIONS(readOnly),
MAINTENANCE_WINDOWS_OPTIONS(readOnly),
ICMP_ADVANCED(readOnly).requestConfig,
KIBANA_SPACES_OPTIONS(readOnly),
],
},
});

View file

@ -5,14 +5,18 @@
* 2.0.
*/
import React, { FC, PropsWithChildren } from 'react';
import React, { FC, PropsWithChildren, useMemo } from 'react';
import { EuiForm, EuiSpacer } from '@elastic/eui';
import { FormProvider } from 'react-hook-form';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import { SpacesContextProps } from '@kbn/spaces-plugin/public';
import { useFormWrapped } from '../../../../../hooks/use_form_wrapped';
import { FormMonitorType, SyntheticsMonitor } from '../types';
import { getDefaultFormFields, formatDefaultFormValues } from './defaults';
import { ActionBar } from './submit';
import { Disclaimer } from './disclaimer';
import { ClientPluginsStart } from '../../../../../plugin';
const getEmptyFunctionComponent: React.FC<SpacesContextProps> = ({ children }) => <>{children}</>;
export const MonitorForm: FC<
PropsWithChildren<{
@ -31,6 +35,14 @@ export const MonitorForm: FC<
shouldFocusError: false,
});
const { spaces: spacesApi } = useKibana<ClientPluginsStart>().services;
const ContextWrapper = useMemo(
() =>
spacesApi ? spacesApi.ui.components.getSpacesContextProvider : getEmptyFunctionComponent,
[spacesApi]
);
/* React hook form doesn't seem to register a field
* as dirty until validation unless dirtyFields is subscribed to */
const {
@ -38,6 +50,7 @@ export const MonitorForm: FC<
} = methods;
return (
<ContextWrapper>
<FormProvider {...methods}>
<EuiForm
isInvalid={Boolean(isSubmitted && Object.keys(errors).length)}
@ -50,5 +63,6 @@ export const MonitorForm: FC<
</EuiForm>
<Disclaimer />
</FormProvider>
</ContextWrapper>
);
};

View file

@ -26,6 +26,11 @@ jest.mock('../../hooks/use_monitor_name', () => ({
useMonitorName: jest.fn().mockReturnValue({ nameAlreadyExists: false }),
}));
jest.mock('../../../../hooks/use_kibana_space', () => ({
...jest.requireActual('../../../../hooks/use_kibana_space'),
useKibanaSpace: jest.fn().mockReturnValue({ id: 'default' }),
}));
describe('MonitorEditPage', () => {
const { FETCH_STATUS } = observabilitySharedPublic;

View file

@ -168,4 +168,5 @@ export interface FieldMap {
[ConfigKey.MAX_ATTEMPTS]: FieldMeta<ConfigKey.MAX_ATTEMPTS>;
[ConfigKey.LABELS]: FieldMeta<ConfigKey.LABELS>;
[ConfigKey.MAINTENANCE_WINDOWS]: FieldMeta<ConfigKey.MAINTENANCE_WINDOWS>;
[ConfigKey.KIBANA_SPACES]: FieldMeta<ConfigKey.KIBANA_SPACES>;
}

View file

@ -11,8 +11,10 @@ import React from 'react';
import { useHistory } from 'react-router-dom';
import { FETCH_STATUS, TagsList } from '@kbn/observability-shared-plugin/public';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import { isEmpty } from 'lodash';
import { ClientPluginsStart } from '../../../../../../plugin';
import { useKibanaSpace } from '../../../../../../hooks/use_kibana_space';
import { useEnablement } from '../../../../hooks';
import { getMonitorSpaceToAppend, useEnablement } from '../../../../hooks';
import { useCanEditSynthetics } from '../../../../../../hooks/use_capabilities';
import {
isStatusEnabled,
@ -49,7 +51,7 @@ export function useMonitorListColumns({
setMonitorPendingDeletion: (configs: string[]) => void;
}): Array<EuiBasicTableColumn<EncryptedSyntheticsSavedMonitor>> {
const history = useHistory();
const { http } = useKibana().services;
const { http, spaces } = useKibana<ClientPluginsStart>().services;
const canEditSynthetics = useCanEditSynthetics();
const { isServiceAllowed } = useEnablement();
@ -69,6 +71,7 @@ export function useMonitorListColumns({
return publicLocations ? Boolean(canUsePublicLocations) : true;
};
const LazySpaceList = spaces?.ui.components.getSpaceList ?? (() => null);
const columns: Array<EuiBasicTableColumn<EncryptedSyntheticsSavedMonitor>> = [
{
@ -171,6 +174,21 @@ export function useMonitorListColumns({
/>
),
},
{
name: i18n.translate('xpack.synthetics.management.monitorList.spacesColumnTitle', {
defaultMessage: 'Spaces',
}),
field: 'spaces',
sortable: false,
render: (monSpaces: string[]) => {
return (
<LazySpaceList
namespaces={monSpaces ?? (space ? [space?.id] : [])}
behaviorContext="outside-space"
/>
);
},
},
{
align: 'right' as const,
name: i18n.translate('xpack.synthetics.management.monitorList.actions', {
@ -206,9 +224,10 @@ export function useMonitorListColumns({
isPublicLocationsAllowed(fields) &&
isServiceAllowed,
href: (fields) => {
if ('spaceId' in fields && space?.id !== fields.spaceId) {
const appendSpaceId = getMonitorSpaceToAppend(space, fields.spaces);
if (!isEmpty(appendSpaceId)) {
return http?.basePath.prepend(
`edit-monitor/${fields[ConfigKey.CONFIG_ID]}?spaceId=${fields.spaceId}`
`edit-monitor/${fields[ConfigKey.CONFIG_ID]}?spaceId=${fields.spaces?.[0]}`
)!;
}
return http?.basePath.prepend(`edit-monitor/${fields[ConfigKey.CONFIG_ID]}`)!;

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import React, { useCallback, useState } from 'react';
import React, { useCallback, useMemo, useState } from 'react';
import {
Criteria,
EuiBasicTable,
@ -16,6 +16,8 @@ import {
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { EuiTableSelectionType } from '@elastic/eui/src/components/basic_table/table_types';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import { SpacesContextProps } from '@kbn/spaces-plugin/public';
import { MonitorListHeader } from './monitor_list_header';
import type { MonitorListSortField } from '../../../../../../../common/runtime_types/monitor_management/sort_field';
import { DeleteMonitor } from './delete_monitor';
@ -29,6 +31,7 @@ import {
} from '../../../../../../../common/runtime_types';
import { useMonitorListColumns } from './columns';
import * as labels from './labels';
import { ClientPluginsStart } from '../../../../../../plugin';
interface Props {
pageState: MonitorListPageState;
@ -40,6 +43,7 @@ interface Props {
reloadPage: () => void;
overviewStatus: OverviewStatusState | null;
}
const getEmptyFunctionComponent: React.FC<SpacesContextProps> = ({ children }) => <>{children}</>;
export const MonitorList = ({
pageState: { pageIndex, pageSize, sortField, sortOrder },
@ -108,9 +112,16 @@ export const MonitorList = ({
onSelectionChange,
initialSelected: selectedItems,
};
const { spaces: spacesApi } = useKibana<ClientPluginsStart>().services;
const ContextWrapper = useMemo(
() =>
spacesApi ? spacesApi.ui.components.getSpacesContextProvider : getEmptyFunctionComponent,
[spacesApi]
);
return (
<>
<ContextWrapper>
<EuiPanel hasBorder={false} hasShadow={false} paddingSize="none">
<MonitorListHeader
recordRangeLabel={recordRangeLabel}
@ -152,6 +163,6 @@ export const MonitorList = ({
reloadPage={reloadPage}
/>
)}
</>
</ContextWrapper>
);
};

View file

@ -116,9 +116,9 @@ export function ActionsPopover({
const detailUrl = useMonitorDetailLocator({
configId: monitor.configId,
locationId: locationId ?? monitor.locationId,
spaceId: monitor.spaceId,
spaces: monitor.spaces,
});
const editUrl = useEditMonitorLocator({ configId: monitor.configId, spaceId: monitor.spaceId });
const editUrl = useEditMonitorLocator({ configId: monitor.configId, spaces: monitor.spaces });
const canEditSynthetics = useCanEditSynthetics();

View file

@ -0,0 +1,69 @@
/*
* 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 React from 'react';
import { EuiHorizontalRule, EuiText, EuiToolTip, EuiSpacer } from '@elastic/eui';
import { Moment } from 'moment';
import { i18n } from '@kbn/i18n';
import {
getShortTimeStamp,
parseTimestamp,
} from '../../../../../../../../../common/utils/date_util';
import {
MonitorTypeEnum,
OverviewStatusMetaData,
} from '../../../../../../../../../common/runtime_types';
import { BadgeStatus } from '../../../../../common/components/monitor_status';
export const MonitorStatusCol = ({
monitor,
openFlyout,
}: {
monitor: OverviewStatusMetaData;
openFlyout: (monitor: OverviewStatusMetaData) => void;
}) => {
const timestamp = monitor.timestamp ? parseTimestamp(monitor.timestamp) : null;
return (
<div>
<BadgeStatus
status={monitor.status}
isBrowserType={monitor.type === MonitorTypeEnum.BROWSER}
onClickBadge={() => openFlyout(monitor)}
/>
<EuiSpacer size="xs" />
{timestamp ? (
<EuiToolTip
content={
<>
<EuiText color="text" size="xs">
<strong> {timestamp.fromNow()}</strong>
</EuiText>
<EuiHorizontalRule margin="xs" />
<EuiText color="ghost" size="xs">
{timestamp.toLocaleString()}
</EuiText>
</>
}
>
<EuiText size="xs" color="subdued" className="eui-textNoWrap">
{getCheckedLabel(timestamp)}
</EuiText>
</EuiToolTip>
) : (
'--'
)}
</div>
);
};
const getCheckedLabel = (timestamp: Moment) => {
return i18n.translate('xpack.synthetics.monitorList.statusColumn.checkedTimestamp', {
defaultMessage: 'Checked {timestamp}',
values: { timestamp: getShortTimeStamp(timestamp) },
});
};

View file

@ -33,7 +33,7 @@ export const MonitorsTable = ({
const getRowProps = useCallback(
(monitor: OverviewStatusMetaData): EuiTableRowProps => {
const { configId, locationLabel, locationId, spaceId } = monitor;
const { configId, locationLabel, locationId, spaces } = monitor;
return {
onClick: (e) => {
// This is a workaround to prevent the flyout from opening when clicking on the action buttons
@ -49,7 +49,7 @@ export const MonitorsTable = ({
id: configId,
location: locationLabel,
locationId,
spaceId,
spaces,
})
);
}

View file

@ -9,16 +9,16 @@ import React, { useCallback, useMemo } from 'react';
import { EuiBasicTableColumn, EuiLink, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui';
import { useHistory } from 'react-router-dom';
import { TagsList } from '@kbn/observability-shared-plugin/public';
import { useDispatch } from 'react-redux';
import { useDispatch, useSelector } from 'react-redux';
import { i18n } from '@kbn/i18n';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import { MonitorStatusCol } from '../components/monitor_status_col';
import { selectOverviewState } from '../../../../../../state';
import { MonitorBarSeries } from '../components/monitor_bar_series';
import { useMonitorHistogram } from '../../../../hooks/use_monitor_histogram';
import {
MonitorTypeEnum,
OverviewStatusMetaData,
} from '../../../../../../../../../common/runtime_types';
import { OverviewStatusMetaData } from '../../../../../../../../../common/runtime_types';
import { MonitorTypeBadge } from '../../../../../common/components/monitor_type_badge';
import { getFilterForTypeMessage } from '../../../../management/monitor_list_table/labels';
import { BadgeStatus } from '../../../../../common/components/monitor_status';
import { FlyoutParamProps } from '../../types';
import { MonitorsActions } from '../components/monitors_actions';
import {
@ -33,6 +33,8 @@ import {
MONITOR_HISTORY,
} from '../labels';
import { MonitorsDuration } from '../components/monitors_duration';
import { useKibanaSpace } from '../../../../../../../../hooks/use_kibana_space';
import { ClientPluginsStart } from '../../../../../../../../plugin';
export const useMonitorsTableColumns = ({
setFlyoutConfigCallback,
@ -43,6 +45,12 @@ export const useMonitorsTableColumns = ({
}) => {
const history = useHistory();
const { histogramsById, minInterval } = useMonitorHistogram({ items });
const { space } = useKibanaSpace();
const { spaces } = useKibana<ClientPluginsStart>().services;
const {
pageState: { showFromAllSpaces },
} = useSelector(selectOverviewState);
const onClickMonitorFilter = useCallback(
(filterName: string, filterValue: string) => {
@ -60,31 +68,28 @@ export const useMonitorsTableColumns = ({
const openFlyout = useCallback(
(monitor: OverviewStatusMetaData) => {
const { configId, locationLabel, locationId, spaceId } = monitor;
const { configId, locationLabel, locationId } = monitor;
dispatch(
setFlyoutConfigCallback({
configId,
id: configId,
location: locationLabel,
locationId,
spaceId,
spaces: monitor.spaces,
})
);
},
[dispatch, setFlyoutConfigCallback]
);
const columns: Array<EuiBasicTableColumn<OverviewStatusMetaData>> = useMemo(
() => [
const columns: Array<EuiBasicTableColumn<OverviewStatusMetaData>> = useMemo(() => {
const LazySpaceList = spaces?.ui.components.getSpaceList ?? (() => null);
return [
{
field: 'status',
name: STATUS,
render: (status: OverviewStatusMetaData['status'], monitor) => (
<BadgeStatus
status={status}
isBrowserType={monitor.type === MonitorTypeEnum.BROWSER}
onClickBadge={() => openFlyout(monitor)}
/>
render: (monitor: OverviewStatusMetaData) => (
<MonitorStatusCol monitor={monitor} openFlyout={openFlyout} />
),
},
{
@ -174,15 +179,41 @@ export const useMonitorsTableColumns = ({
);
},
},
...(showFromAllSpaces
? [
{
name: i18n.translate('xpack.synthetics.management.monitorList.spacesColumnTitle', {
defaultMessage: 'Spaces',
}),
field: 'spaces',
sortable: true,
render: (monSpaces: string[]) => {
return (
<LazySpaceList
namespaces={monSpaces ?? (space ? [space?.id] : [])}
behaviorContext="outside-space"
/>
);
},
},
]
: []),
{
name: ACTIONS,
render: (monitor: OverviewStatusMetaData) => <MonitorsActions monitor={monitor} />,
align: 'right',
width: '40px',
},
],
[histogramsById, minInterval, onClickMonitorFilter, openFlyout]
);
];
}, [
histogramsById,
minInterval,
onClickMonitorFilter,
openFlyout,
showFromAllSpaces,
space,
spaces?.ui.components.getSpaceList,
]);
return {
columns,

View file

@ -33,7 +33,7 @@ import { MetricItemExtra } from './metric_item_extra';
import { MetricItemIcon } from './metric_item_icon';
import { FlyoutParamProps } from '../types';
const METRIC_ITEM_HEIGHT = 160;
const METRIC_ITEM_HEIGHT = 170;
export const getColor = (euiTheme: EuiThemeComputed, isEnabled: boolean, status?: string) => {
if (!isEnabled) {
@ -171,7 +171,7 @@ export const MetricItem = ({
id: monitor.configId,
location: locationName,
locationId: monitor.locationId,
spaceId: monitor.spaceId,
spaces: monitor.spaces,
});
}
}}

View file

@ -60,7 +60,7 @@ interface Props {
id: string;
location: string;
locationId: string;
spaceId?: string;
spaces?: string[];
onClose: () => void;
onEnabledChange: () => void;
onLocationChange: (params: FlyoutParamProps) => void;
@ -220,7 +220,7 @@ export function LoadingState() {
}
export function MonitorDetailFlyout(props: Props) {
const { id, configId, onLocationChange, locationId, spaceId } = props;
const { id, configId, onLocationChange, locationId, spaces } = props;
const { status: overviewStatus } = useOverviewStatus({ scopeStatusByLocation: true });
@ -235,13 +235,14 @@ export function MonitorDetailFlyout(props: Props) {
const setLocation = useCallback(
(location: string, locationIdT: string) =>
onLocationChange({ id, configId, location, locationId: locationIdT, spaceId }),
[onLocationChange, id, configId, spaceId]
onLocationChange({ id, configId, location, locationId: locationIdT, spaces }),
[onLocationChange, id, configId, spaces]
);
const detailLink = useMonitorDetailLocator({
configId,
locationId,
spaces,
});
const dispatch = useDispatch();
@ -265,10 +266,10 @@ export function MonitorDetailFlyout(props: Props) {
dispatch(
getMonitorAction.get({
monitorId: configId,
...(spaceId && spaceId !== space?.id ? { spaceId } : {}),
...(space && spaces?.length && !spaces?.includes(space?.id) ? { spaceId: spaces[0] } : {}),
})
);
}, [configId, dispatch, space?.id, spaceId, upsertSuccess]);
}, [configId, dispatch, space, space?.id, spaces, upsertSuccess]);
const [isActionsPopoverOpen, setIsActionsPopoverOpen] = useState(false);
@ -392,7 +393,7 @@ export const MaybeMonitorDetailsFlyout = ({
id={flyoutConfig.id}
location={flyoutConfig.location}
locationId={flyoutConfig.locationId}
spaceId={flyoutConfig.spaceId}
spaces={flyoutConfig.spaces}
onClose={hideFlyout}
onEnabledChange={forceRefreshCallback}
onLocationChange={setFlyoutConfigCallback}

View file

@ -44,7 +44,7 @@ import { MaybeMonitorDetailsFlyout } from './monitor_detail_flyout';
import { OverviewGridCompactView } from './compact_view/overview_grid_compact_view';
import { ViewButtons } from './view_buttons/view_buttons';
const ITEM_HEIGHT = 172;
const ITEM_HEIGHT = 182;
const ROW_COUNT = 4;
const MAX_LIST_HEIGHT = 800;
const MIN_BATCH_SIZE = 20;
@ -189,7 +189,7 @@ export const OverviewGrid = memo(
<EuiFlexGroup
data-test-subj={`overview-grid-row-${listIndex}`}
gutterSize="m"
css={{ ...style }}
css={{ ...style, marginLeft: 5 }}
>
{listData[listIndex].map((_, idx) => (
<EuiFlexItem

View file

@ -10,5 +10,5 @@ export interface FlyoutParamProps {
configId: string;
location: string;
locationId: string;
spaceId?: string;
spaces?: string[];
}

View file

@ -38,7 +38,7 @@ export const SpaceSelector = <T extends FieldValues>({
const showFieldInvalid = (isSubmitted || isTouched) && !!error;
useEffect(() => {
if (data) {
if (data?.spacesDataPromise) {
data.spacesDataPromise.then((spacesData) => {
setSpacesList([
allSpacesOption,

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import React from 'react';
import React, { useMemo } from 'react';
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
import { EuiThemeProvider } from '@kbn/kibana-react-plugin/common';
import { Provider as ReduxProvider } from 'react-redux';
@ -13,16 +13,26 @@ import { RedirectAppLinks } from '@kbn/shared-ux-link-redirect-app';
import { Subject } from 'rxjs';
import { Store } from 'redux';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { SpacesContextProps } from '@kbn/spaces-plugin/public';
import { SyntheticsRefreshContextProvider } from './synthetics_refresh_context';
import { SyntheticsDataViewContextProvider } from './synthetics_data_view_context';
import { SyntheticsAppProps } from './synthetics_settings_context';
import { storage, store } from '../state';
const getEmptyFunctionComponent: React.FC<SpacesContextProps> = ({ children }) => <>{children}</>;
export const SyntheticsSharedContext: React.FC<
React.PropsWithChildren<SyntheticsAppProps & { reload$?: Subject<boolean>; reduxStore?: Store }>
> = ({ reduxStore, coreStart, setupPlugins, startPlugins, children, darkMode, reload$ }) => {
const queryClient = new QueryClient();
const spacesApi = startPlugins.spaces;
const ContextWrapper = useMemo(
() =>
spacesApi ? spacesApi.ui.components.getSpacesContextProvider : getEmptyFunctionComponent,
[spacesApi]
);
return (
<KibanaContextProvider
services={{
@ -60,7 +70,7 @@ export const SyntheticsSharedContext: React.FC<
height: '100%',
}}
>
{children}
<ContextWrapper>{children}</ContextWrapper>
</RedirectAppLinks>
</SyntheticsDataViewContextProvider>
</SyntheticsRefreshContextProvider>

View file

@ -9,16 +9,25 @@ import { useEffect, useState } from 'react';
import { LocatorClient } from '@kbn/share-plugin/common/url_service/locators';
import { syntheticsEditMonitorLocatorID } from '@kbn/observability-plugin/common';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import type { Space } from '@kbn/spaces-plugin/common';
import { ALL_SPACES_ID } from '@kbn/security-plugin/public';
import { useKibanaSpace } from '../../../hooks/use_kibana_space';
import { ClientPluginsStart } from '../../../plugin';
export const getMonitorSpaceToAppend = (space?: Space, spaces?: string[]) => {
if (spaces?.includes(ALL_SPACES_ID)) {
return {};
}
return space && spaces?.length && !spaces?.includes(space?.id) ? { spaceId: spaces[0] } : {};
};
export function useEditMonitorLocator({
configId,
locators,
spaceId,
spaces,
}: {
configId: string;
spaceId?: string;
spaces?: string[];
locators?: LocatorClient;
}) {
const { space } = useKibanaSpace();
@ -31,12 +40,12 @@ export function useEditMonitorLocator({
async function generateUrl() {
const url = await locator?.getUrl({
configId,
...(spaceId && spaceId !== space?.id ? { spaceId } : {}),
...getMonitorSpaceToAppend(space, spaces),
});
setEditUrl(url);
}
generateUrl();
}, [locator, configId, space, spaceId]);
}, [locator, configId, space, spaces]);
return editUrl;
}

View file

@ -8,17 +8,18 @@
import { useEffect, useState } from 'react';
import { syntheticsMonitorDetailLocatorID } from '@kbn/observability-plugin/common';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import { getMonitorSpaceToAppend } from './use_edit_monitor_locator';
import { useKibanaSpace } from '../../../hooks/use_kibana_space';
import { ClientPluginsStart } from '../../../plugin';
export function useMonitorDetailLocator({
configId,
locationId,
spaceId,
spaces,
}: {
configId: string;
locationId?: string;
spaceId?: string;
spaces?: string[];
}) {
const { space } = useKibanaSpace();
const [monitorUrl, setMonitorUrl] = useState<string | undefined>(undefined);
@ -31,12 +32,12 @@ export function useMonitorDetailLocator({
const url = await locator?.getUrl({
configId,
locationId,
...(spaceId && spaceId !== space?.id ? { spaceId } : {}),
...getMonitorSpaceToAppend(space, spaces),
});
setMonitorUrl(url);
}
generateUrl();
}, [locator, configId, locationId, spaceId, space?.id]);
}, [locator, configId, locationId, spaces, space?.id, space]);
return monitorUrl;
}

View file

@ -72,7 +72,7 @@ class ApiService {
}
private parseApiUrl(apiUrl: string, spaceId?: string) {
if (spaceId) {
if (spaceId && spaceId !== 'default' && spaceId !== '*') {
const basePath = kibanaService.coreSetup.http.basePath;
return addSpaceIdToPath(basePath.serverBasePath, spaceId, apiUrl);
}

View file

@ -8,6 +8,7 @@
import { KueryNode, fromKueryExpression, toElasticsearchQuery } from '@kbn/es-query';
import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
import { SyntheticsMonitorStatusRuleParams as StatusRuleParams } from '@kbn/response-ops-rule-params/synthetics_monitor_status';
import { ALL_SPACES_ID } from '@kbn/security-plugin/common/constants';
import { SyntheticsEsClient } from '../../../lib';
import {
FINAL_SUMMARY_FILTER,
@ -48,8 +49,8 @@ export async function queryFilterMonitors({
getRangeFilter({ from: 'now-24h/m', to: 'now/m' }),
getTimeSpanFilter(),
{
term: {
'meta.space_id': spaceId,
terms: {
'meta.space_id': [spaceId, ALL_SPACES_ID],
},
},
{

View file

@ -8,13 +8,13 @@
import moment from 'moment';
import { SavedObjectsFindResult } from '@kbn/core/server';
import { Logger } from '@kbn/core/server';
import { MonitorData } from '../../../saved_objects/synthetics_monitor/get_all_monitors';
import { MonitorData } from '../../../saved_objects/synthetics_monitor/process_monitors';
import {
AlertStatusConfigs,
AlertPendingStatusConfigs,
MissingPingMonitorInfo,
} from '../../../../common/runtime_types/alert_rules/common';
import { EncryptedSyntheticsMonitorAttributes } from '../../../../common/runtime_types';
import { ConfigKey, EncryptedSyntheticsMonitorAttributes } from '../../../../common/runtime_types';
export interface ConfigStats {
up: number;
@ -31,7 +31,10 @@ export const getMissingPingMonitorInfo = ({
configId: string;
locationId: string;
}): (MissingPingMonitorInfo & { createdAt?: string }) | undefined => {
const monitor = monitors.find((m) => m.id === configId);
const monitor = monitors.find(
// for project monitors, we can match by id or by monitor query id
(m) => m.id === configId || m.attributes[ConfigKey.MONITOR_QUERY_ID] === configId
);
if (!monitor) {
// This should never happen
return;

View file

@ -10,7 +10,7 @@ import { times } from 'lodash';
import { intersection } from 'lodash';
import { SavedObjectsFindResult } from '@kbn/core/server';
import { Logger } from '@kbn/core/server';
import { MonitorData } from '../../../saved_objects/synthetics_monitor/get_all_monitors';
import { MonitorData } from '../../../saved_objects/synthetics_monitor/process_monitors';
import {
AlertStatusConfigs,
AlertStatusMetaData,

View file

@ -96,7 +96,7 @@ describe('StatusRuleExecutor', () => {
expect(staleDownConfigs).toEqual({});
expect(spy).toHaveBeenCalledWith({
filter: 'synthetics-monitor.attributes.alert.status.enabled: true',
filter: 'synthetics-monitor-multi-space.attributes.alert.status.enabled: true',
});
});

View file

@ -13,6 +13,7 @@ import { Logger } from '@kbn/core/server';
import { intersection, isEmpty } from 'lodash';
import { getAlertDetailsUrl } from '@kbn/observability-plugin/common';
import { SyntheticsMonitorStatusRuleParams as StatusRuleParams } from '@kbn/response-ops-rule-params/synthetics_monitor_status';
import { syntheticsMonitorAttributes } from '../../../common/types/saved_objects';
import { MonitorConfigRepository } from '../../services/monitor_config_repository';
import {
AlertOverviewStatus,
@ -40,11 +41,10 @@ import { queryMonitorStatusAlert } from './queries/query_monitor_status_alert';
import { parseArrayFilters, parseLocationFilter } from '../../routes/common';
import { SyntheticsServerSetup } from '../../types';
import { SyntheticsEsClient } from '../../lib';
import { processMonitors } from '../../saved_objects/synthetics_monitor/get_all_monitors';
import { processMonitors } from '../../saved_objects/synthetics_monitor/process_monitors';
import { getConditionType } from '../../../common/rules/status_rule';
import { ConfigKey, EncryptedSyntheticsMonitorAttributes } from '../../../common/runtime_types';
import { SyntheticsMonitorClient } from '../../synthetics_service/synthetics_monitor/synthetics_monitor_client';
import { monitorAttributes } from '../../../common/types/saved_objects';
import { AlertConfigKey } from '../../../common/constants/monitor_management';
import { ALERT_DETAILS_URL, VIEW_IN_APP_URL } from '../action_variables';
import { MONITOR_STATUS } from '../../../common/constants/synthetics_alerts';
@ -105,7 +105,7 @@ export class StatusRuleExecutor {
async getMonitors() {
const baseFilter = !this.hasCustomCondition
? `${monitorAttributes}.${AlertConfigKey.STATUS_ENABLED}: true`
? `${syntheticsMonitorAttributes}.${AlertConfigKey.STATUS_ENABLED}: true`
: '';
const configIds = await queryFilterMonitors({

View file

@ -62,7 +62,7 @@ describe('tlsRuleExecutor', () => {
const monitorClient = new SyntheticsMonitorClient(syntheticsService, serverMock);
const commonFilter =
'synthetics-monitor.attributes.alert.tls.enabled: true and (synthetics-monitor.attributes.type: http or synthetics-monitor.attributes.type: tcp)';
'synthetics-monitor-multi-space.attributes.alert.tls.enabled: true and (synthetics-monitor-multi-space.attributes.type: http or synthetics-monitor-multi-space.attributes.type: tcp)';
const getTLSRuleExecutorParams = (
ruleParams: TLSRuleParams = {}
@ -110,7 +110,7 @@ describe('tlsRuleExecutor', () => {
await tlsRule.getMonitors();
expect(getAllMock).toHaveBeenCalledWith({
filter: `${commonFilter} AND synthetics-monitor.attributes.id:(\"${monitorId}\")`,
filter: `${commonFilter} AND synthetics-monitor-multi-space.attributes.id:(\"${monitorId}\")`,
});
});
@ -123,7 +123,7 @@ describe('tlsRuleExecutor', () => {
await tlsRule.getMonitors();
expect(getAllMock).toHaveBeenCalledWith({
filter: `${commonFilter} AND synthetics-monitor.attributes.tags:(\"${tag}\")`,
filter: `${commonFilter} AND synthetics-monitor-multi-space.attributes.tags:(\"${tag}\")`,
});
});
@ -138,7 +138,7 @@ describe('tlsRuleExecutor', () => {
await tlsRule.getMonitors();
expect(getAllMock).toHaveBeenCalledWith({
filter: `${commonFilter} AND synthetics-monitor.attributes.type:(\"${monitorType}\")`,
filter: `${commonFilter} AND synthetics-monitor-multi-space.attributes.type:(\"${monitorType}\")`,
});
});

View file

@ -14,15 +14,16 @@ import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
import type { TLSRuleParams } from '@kbn/response-ops-rule-params/synthetics_tls';
import moment from 'moment';
import { isEmpty } from 'lodash';
import { getSyntheticsDynamicSettings } from '../../saved_objects/synthetics_settings';
import { syntheticsMonitorAttributes } from '../../../common/types/saved_objects';
import { TLSRuleInspect } from '../../../common/runtime_types/alert_rules/common';
import { MonitorConfigRepository } from '../../services/monitor_config_repository';
import { FINAL_SUMMARY_FILTER } from '../../../common/constants/client_defaults';
import { formatFilterString } from '../common';
import { SyntheticsServerSetup } from '../../types';
import { getSyntheticsCerts } from '../../queries/get_certs';
import { savedObjectsAdapter } from '../../saved_objects';
import { DYNAMIC_SETTINGS_DEFAULTS, SYNTHETICS_INDEX_PATTERN } from '../../../common/constants';
import { processMonitors } from '../../saved_objects/synthetics_monitor/get_all_monitors';
import { processMonitors } from '../../saved_objects/synthetics_monitor/process_monitors';
import {
CertResult,
ConfigKey,
@ -30,7 +31,6 @@ import {
Ping,
} from '../../../common/runtime_types';
import { SyntheticsMonitorClient } from '../../synthetics_service/synthetics_monitor/synthetics_monitor_client';
import { monitorAttributes } from '../../../common/types/saved_objects';
import { AlertConfigKey } from '../../../common/constants/monitor_management';
import { SyntheticsEsClient } from '../../lib';
import { queryFilterMonitors } from '../status_rule/queries/filter_monitors';
@ -81,9 +81,9 @@ export class TLSRuleExecutor {
}
async getMonitors() {
const HTTP_OR_TCP = `${monitorAttributes}.${ConfigKey.MONITOR_TYPE}: http or ${monitorAttributes}.${ConfigKey.MONITOR_TYPE}: tcp`;
const HTTP_OR_TCP = `${syntheticsMonitorAttributes}.${ConfigKey.MONITOR_TYPE}: http or ${syntheticsMonitorAttributes}.${ConfigKey.MONITOR_TYPE}: tcp`;
const baseFilter = `${monitorAttributes}.${AlertConfigKey.TLS_ENABLED}: true and (${HTTP_OR_TCP})`;
const baseFilter = `${syntheticsMonitorAttributes}.${AlertConfigKey.TLS_ENABLED}: true and (${HTTP_OR_TCP})`;
const configIds = await queryFilterMonitors({
spaceId: this.spaceId,
@ -135,7 +135,7 @@ export class TLSRuleExecutor {
async getExpiredCertificates() {
const { enabledMonitorQueryIds } = await this.getMonitors();
const dynamicSettings = await savedObjectsAdapter.getSyntheticsDynamicSettings(this.soClient);
const dynamicSettings = await getSyntheticsDynamicSettings(this.soClient);
const expiryThreshold =
this.params.certExpirationThreshold ??

View file

@ -15,11 +15,16 @@ import { ALERTING_FEATURE_ID } from '@kbn/alerting-plugin/common';
import { DEPRECATED_ALERTING_CONSUMERS } from '@kbn/rule-data-utils';
import { UPTIME_RULE_TYPE_IDS, SYNTHETICS_RULE_TYPE_IDS } from '@kbn/rule-data-utils';
import { KibanaFeatureScope } from '@kbn/features-plugin/common';
import { syntheticsMonitorType, syntheticsParamType } from '../common/types/saved_objects';
import {
legacyPrivateLocationsSavedObjectName,
privateLocationSavedObjectName,
} from '../common/saved_objects/private_locations';
import {
legacySyntheticsMonitorTypeSingle,
syntheticsMonitorSavedObjectType,
syntheticsParamType,
} from '../common/types/saved_objects';
import { PLUGIN } from '../common/constants/plugin';
import {
syntheticsSettingsObjectType,
@ -93,7 +98,8 @@ export const syntheticsFeature = {
savedObject: {
all: [
syntheticsSettingsObjectType,
syntheticsMonitorType,
legacySyntheticsMonitorTypeSingle,
syntheticsMonitorSavedObjectType,
syntheticsApiKeyObjectType,
syntheticsParamType,
@ -124,7 +130,8 @@ export const syntheticsFeature = {
read: [
syntheticsParamType,
syntheticsSettingsObjectType,
syntheticsMonitorType,
syntheticsMonitorSavedObjectType,
legacySyntheticsMonitorTypeSingle,
syntheticsApiKeyObjectType,
privateLocationSavedObjectName,
legacyPrivateLocationsSavedObjectName,

View file

@ -0,0 +1,38 @@
/*
* 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 { KibanaRequest } from '@kbn/core-http-server';
import { SavedObjectsClientContract } from '@kbn/core-saved-objects-api-server';
import { MonitorConfigRepository } from '../services/monitor_config_repository';
import { SyntheticsServerSetup } from '../types';
import { SyntheticsService } from '../synthetics_service/synthetics_service';
import { SyntheticsMonitorClient } from '../synthetics_service/synthetics_monitor/synthetics_monitor_client';
import { getServerMock } from './server_mock';
export const getRouteContextMock = () => {
const serverMock: SyntheticsServerSetup = getServerMock();
const syntheticsService = new SyntheticsService(serverMock);
const monitorConfigRepo = new MonitorConfigRepository(
serverMock.authSavedObjectsClient as unknown as SavedObjectsClientContract,
serverMock.encryptedSavedObjects.getClient()
);
const syntheticsMonitorClient = new SyntheticsMonitorClient(syntheticsService, serverMock);
return {
routeContext: {
syntheticsMonitorClient,
server: serverMock,
request: {} as unknown as KibanaRequest,
savedObjectsClient:
serverMock.authSavedObjectsClient as unknown as SavedObjectsClientContract,
monitorConfigRepository: monitorConfigRepo,
} as any,
syntheticsService,
serverMock,
};
};

View file

@ -0,0 +1,51 @@
/*
* 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 { loggerMock } from '@kbn/logging-mocks';
import { SyntheticsServerSetup } from '../types';
import { mockEncryptedSO } from '../synthetics_service/utils/mocks';
export const getServerMock = () => {
const logger = loggerMock.create();
const serverMock: SyntheticsServerSetup = {
syntheticsEsClient: { search: jest.fn() },
stackVersion: null,
authSavedObjectsClient: {
bulkUpdate: jest.fn(),
get: jest.fn(),
update: jest.fn(),
createPointInTimeFinder: jest.fn().mockImplementation(({ perPage, type: soType }) => ({
close: jest.fn(async () => {}),
find: jest.fn().mockReturnValue({
async *[Symbol.asyncIterator]() {
yield {
saved_objects: [],
};
},
}),
})),
},
logger,
config: {
service: {
username: 'dev',
password: '12345',
},
},
fleet: {
packagePolicyService: {
get: jest.fn().mockReturnValue({}),
getByIDs: jest.fn().mockReturnValue([]),
buildPackagePolicyFromPackage: jest.fn().mockReturnValue({}),
},
},
encryptedSavedObjects: mockEncryptedSO(),
} as unknown as SyntheticsServerSetup;
return serverMock;
};

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import * as getAllMonitors from '../../saved_objects/synthetics_monitor/get_all_monitors';
import * as getAllMonitors from '../../saved_objects/synthetics_monitor/process_monitors';
import * as getCerts from '../../queries/get_certs';
import { getSyntheticsCertsRoute } from './get_certificates';
import { MonitorConfigRepository } from '../../services/monitor_config_repository';

View file

@ -6,9 +6,9 @@
*/
import { schema } from '@kbn/config-schema';
import { syntheticsMonitorAttributes } from '../../../common/types/saved_objects';
import { SyntheticsRestApiRouteFactory } from '../types';
import { processMonitors } from '../../saved_objects/synthetics_monitor/get_all_monitors';
import { monitorAttributes } from '../../../common/types/saved_objects';
import { processMonitors } from '../../saved_objects/synthetics_monitor/process_monitors';
import { SYNTHETICS_API_URLS } from '../../../common/constants';
import { CertResult, GetCertsParams } from '../../../common/runtime_types';
import { ConfigKey } from '../../../common/constants/monitor_management';
@ -35,7 +35,7 @@ export const getSyntheticsCertsRoute: SyntheticsRestApiRouteFactory<
const queryParams = request.query;
const monitors = await monitorConfigRepository.getAll({
filter: `${monitorAttributes}.${ConfigKey.ENABLED}: true`,
filter: `${syntheticsMonitorAttributes}.${ConfigKey.ENABLED}: true`,
});
if (monitors.length === 0) {

View file

@ -14,7 +14,7 @@ describe('common utils', () => {
configIds: ['1 4', '2 6', '5'],
});
expect(filters.filtersStr).toMatchInlineSnapshot(
`"synthetics-monitor.attributes.config_id:(\\"1 4\\" OR \\"2 6\\" OR \\"5\\")"`
`"synthetics-monitor-multi-space.attributes.config_id:(\\"1 4\\" OR \\"2 6\\" OR \\"5\\")"`
);
});
it('tests parseArrayFilters with tags and configIds', () => {
@ -23,7 +23,7 @@ describe('common utils', () => {
tags: ['tag1', 'tag2'],
});
expect(filters.filtersStr).toMatchInlineSnapshot(
`"synthetics-monitor.attributes.tags:(\\"tag1\\" OR \\"tag2\\") AND synthetics-monitor.attributes.config_id:(\\"1\\" OR \\"2\\")"`
`"synthetics-monitor-multi-space.attributes.tags:(\\"tag1\\" OR \\"tag2\\") AND synthetics-monitor-multi-space.attributes.config_id:(\\"1\\" OR \\"2\\")"`
);
});
it('tests parseArrayFilters with all options', () => {
@ -37,7 +37,7 @@ describe('common utils', () => {
schedules: ['schedule1', 'schedule2'],
});
expect(filters.filtersStr).toMatchInlineSnapshot(
`"synthetics-monitor.attributes.tags:(\\"tag1\\" OR \\"tag2\\") AND synthetics-monitor.attributes.project_id:(\\"project1\\" OR \\"project2\\") AND synthetics-monitor.attributes.type:(\\"type1\\" OR \\"type2\\") AND synthetics-monitor.attributes.locations.id:(\\"loc1\\" OR \\"loc2\\") AND synthetics-monitor.attributes.schedule.number:(\\"schedule1\\" OR \\"schedule2\\") AND synthetics-monitor.attributes.id:(\\"query1\\" OR \\"query2\\") AND synthetics-monitor.attributes.config_id:(\\"1\\" OR \\"2\\")"`
`"synthetics-monitor-multi-space.attributes.tags:(\\"tag1\\" OR \\"tag2\\") AND synthetics-monitor-multi-space.attributes.project_id:(\\"project1\\" OR \\"project2\\") AND synthetics-monitor-multi-space.attributes.type:(\\"type1\\" OR \\"type2\\") AND synthetics-monitor-multi-space.attributes.locations.id:(\\"loc1\\" OR \\"loc2\\") AND synthetics-monitor-multi-space.attributes.schedule.number:(\\"schedule1\\" OR \\"schedule2\\") AND synthetics-monitor-multi-space.attributes.id:(\\"query1\\" OR \\"query2\\") AND synthetics-monitor-multi-space.attributes.config_id:(\\"1\\" OR \\"2\\")"`
);
});
});
@ -49,7 +49,7 @@ describe('getSavedObjectKqlFilter', () => {
it('returns KQL string if values are provided', () => {
expect(getSavedObjectKqlFilter({ field: 'tags', values: 'apm' })).toBe(
'synthetics-monitor.attributes.tags:"apm"'
'synthetics-monitor-multi-space.attributes.tags:"apm"'
);
});
@ -61,13 +61,13 @@ describe('getSavedObjectKqlFilter', () => {
it('handles array values', () => {
expect(getSavedObjectKqlFilter({ field: 'tags', values: ['apm', 'synthetics'] })).toBe(
'synthetics-monitor.attributes.tags:("apm" OR "synthetics")'
'synthetics-monitor-multi-space.attributes.tags:("apm" OR "synthetics")'
);
});
it('escapes quotes', () => {
expect(getSavedObjectKqlFilter({ field: 'tags', values: ['"apm', 'synthetics'] })).toBe(
'synthetics-monitor.attributes.tags:("\\"apm" OR "synthetics")'
'synthetics-monitor-multi-space.attributes.tags:("\\"apm" OR "synthetics")'
);
});
});

View file

@ -6,7 +6,6 @@
*/
import { schema, Type, TypeOf } from '@kbn/config-schema';
import { SavedObjectsFindResponse } from '@kbn/core/server';
import { isEmpty } from 'lodash';
import { escapeQuotes } from '@kbn/es-query';
import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common';
@ -14,9 +13,8 @@ import { useLogicalAndFields } from '../../common/constants';
import { RouteContext } from './types';
import { MonitorSortFieldSchema } from '../../common/runtime_types/monitor_management/sort_field';
import { getAllLocations } from '../synthetics_service/get_all_locations';
import { EncryptedSyntheticsMonitorAttributes } from '../../common/runtime_types';
import { PrivateLocation, ServiceLocation } from '../../common/runtime_types';
import { monitorAttributes } from '../../common/types/saved_objects';
import { syntheticsMonitorAttributes } from '../../common/types/saved_objects';
const StringOrArraySchema = schema.maybe(
schema.oneOf([schema.string(), schema.arrayOf(schema.string())])
@ -75,36 +73,6 @@ export const SEARCH_FIELDS = [
'project_id.text',
];
export const getMonitors = async (
context: RouteContext<MonitorsQuery>,
{ fields }: { fields?: string[] } = {}
): Promise<SavedObjectsFindResponse<EncryptedSyntheticsMonitorAttributes>> => {
const {
perPage = 50,
page,
sortField,
sortOrder,
query,
searchAfter,
showFromAllSpaces,
} = context.request.query;
const { filtersStr } = await getMonitorFilters(context);
return context.monitorConfigRepository.find({
perPage,
page,
sortField: parseMappingKey(sortField),
sortOrder,
searchFields: SEARCH_FIELDS,
search: query,
filter: filtersStr,
searchAfter,
fields,
...(showFromAllSpaces && { namespaces: ['*'] }),
});
};
interface Filters {
filter?: string;
tags?: string | string[];
@ -117,7 +85,8 @@ interface Filters {
}
export const getMonitorFilters = async (
context: RouteContext<Record<string, any>, OverviewStatusQuery>
context: RouteContext<Record<string, any>, OverviewStatusQuery>,
attr: string = syntheticsMonitorAttributes
) => {
const {
tags,
@ -141,7 +110,8 @@ export const getMonitorFilters = async (
monitorQueryIds,
locations,
},
useLogicalAndFor
useLogicalAndFor,
attr
);
};
@ -156,7 +126,8 @@ export const parseArrayFilters = (
monitorQueryIds,
locations,
}: Filters,
useLogicalAndFor: MonitorsQuery['useLogicalAndFor'] = []
useLogicalAndFor: MonitorsQuery['useLogicalAndFor'] = [],
attributes: string = syntheticsMonitorAttributes
) => {
const filtersStr = [
filter,
@ -164,17 +135,19 @@ export const parseArrayFilters = (
field: 'tags',
values: tags,
operator: useLogicalAndFor.includes('tags') ? 'AND' : 'OR',
attributes,
}),
getSavedObjectKqlFilter({ field: 'project_id', values: projects }),
getSavedObjectKqlFilter({ field: 'type', values: monitorTypes }),
getSavedObjectKqlFilter({ field: 'project_id', values: projects, attributes }),
getSavedObjectKqlFilter({ field: 'type', values: monitorTypes, attributes }),
getSavedObjectKqlFilter({
field: 'locations.id',
values: locations,
operator: useLogicalAndFor.includes('locations') ? 'AND' : 'OR',
attributes,
}),
getSavedObjectKqlFilter({ field: 'schedule.number', values: schedules }),
getSavedObjectKqlFilter({ field: 'id', values: monitorQueryIds }),
getSavedObjectKqlFilter({ field: 'config_id', values: configIds }),
getSavedObjectKqlFilter({ field: 'schedule.number', values: schedules, attributes }),
getSavedObjectKqlFilter({ field: 'id', values: monitorQueryIds, attributes }),
getSavedObjectKqlFilter({ field: 'config_id', values: configIds, attributes }),
]
.filter((f) => !!f)
.join(' AND ');
@ -187,11 +160,13 @@ export const getSavedObjectKqlFilter = ({
values,
operator = 'OR',
searchAtRoot = false,
attributes = syntheticsMonitorAttributes,
}: {
field: string;
values?: string | string[];
operator?: string;
searchAtRoot?: boolean;
attributes?: string;
}) => {
if (values === 'All' || (Array.isArray(values) && values?.includes('All'))) {
return undefined;
@ -204,7 +179,7 @@ export const getSavedObjectKqlFilter = ({
if (searchAtRoot) {
fieldKey = `${field}`;
} else {
fieldKey = `${monitorAttributes}.${field}`;
fieldKey = `${attributes}.${field}`;
}
if (Array.isArray(values)) {

View file

@ -8,8 +8,8 @@
import { SavedObjectsClientContract } from '@kbn/core-saved-objects-api-server';
import { parseDuration } from '@kbn/alerting-plugin/server';
import { FindActionResult } from '@kbn/actions-plugin/server';
import { getSyntheticsDynamicSettings } from '../../saved_objects/synthetics_settings';
import { DynamicSettingsAttributes } from '../../runtime_types/settings';
import { savedObjectsAdapter } from '../../saved_objects';
import { populateAlertActions } from '../../../common/rules/alert_actions';
import {
SyntheticsMonitorStatusTranslations,
@ -40,7 +40,7 @@ export class DefaultAlertService {
async getSettings() {
if (!this.settings) {
this.settings = await savedObjectsAdapter.getSyntheticsDynamicSettings(this.soClient);
this.settings = await getSyntheticsDynamicSettings(this.soClient);
}
return this.settings;
}
@ -254,7 +254,7 @@ export class DefaultAlertService {
async getActionConnectors() {
const actionsClient = (await this.context.actions)?.getActionsClient();
if (!this.settings) {
this.settings = await savedObjectsAdapter.getSyntheticsDynamicSettings(this.soClient);
this.settings = await getSyntheticsDynamicSettings(this.soClient);
}
let actionConnectors: FindActionResult[] = [];
try {

View file

@ -5,25 +5,21 @@
* 2.0.
*/
import { getSyntheticsDynamicSettings } from '../../saved_objects/synthetics_settings';
import { DefaultAlertService } from './default_alert_service';
import { SyntheticsRestApiRouteFactory } from '../types';
import { SYNTHETICS_API_URLS } from '../../../common/constants';
import { savedObjectsAdapter } from '../../saved_objects';
import { DEFAULT_ALERT_RESPONSE } from '../../../common/types/default_alerts';
export const updateDefaultAlertingRoute: SyntheticsRestApiRouteFactory = () => ({
method: 'PUT',
path: SYNTHETICS_API_URLS.ENABLE_DEFAULT_ALERTING,
validate: {},
handler: async ({
request,
context,
server,
savedObjectsClient,
}): Promise<DEFAULT_ALERT_RESPONSE> => {
handler: async ({ context, server, savedObjectsClient }): Promise<DEFAULT_ALERT_RESPONSE> => {
const defaultAlertService = new DefaultAlertService(context, server, savedObjectsClient);
const { defaultTLSRuleEnabled, defaultStatusRuleEnabled } =
await savedObjectsAdapter.getSyntheticsDynamicSettings(savedObjectsClient);
const { defaultTLSRuleEnabled, defaultStatusRuleEnabled } = await getSyntheticsDynamicSettings(
savedObjectsClient
);
const updateStatusRulePromise = defaultAlertService.updateStatusRule(defaultStatusRuleEnabled);
const updateTLSRulePromise = defaultAlertService.updateTlsRule(defaultTLSRuleEnabled);

View file

@ -6,7 +6,11 @@
*/
import { schema } from '@kbn/config-schema';
import { SyntheticsRestApiRouteFactory } from '../types';
import { monitorAttributes, syntheticsMonitorType } from '../../../common/types/saved_objects';
import {
legacySyntheticsMonitorTypeSingle,
syntheticsMonitorAttributes,
syntheticsMonitorSavedObjectType,
} from '../../../common/types/saved_objects';
import { ConfigKey, MonitorFiltersResult } from '../../../common/runtime_types';
import { SYNTHETICS_API_URLS } from '../../../common/constants';
@ -44,7 +48,7 @@ export const getSyntheticsFilters: SyntheticsRestApiRouteFactory<MonitorFiltersR
handler: async ({ savedObjectsClient, request }): Promise<any> => {
const showFromAllSpaces = request.query?.showFromAllSpaces;
const data = await savedObjectsClient.find({
type: syntheticsMonitorType,
type: [legacySyntheticsMonitorTypeSingle, syntheticsMonitorSavedObjectType],
perPage: 0,
aggs,
...(showFromAllSpaces ? { namespaces: ['*'] } : {}),
@ -87,31 +91,31 @@ export const getSyntheticsFilters: SyntheticsRestApiRouteFactory<MonitorFiltersR
const aggs = {
monitorTypes: {
terms: {
field: `${monitorAttributes}.${ConfigKey.MONITOR_TYPE}.keyword`,
field: `${syntheticsMonitorAttributes}.${ConfigKey.MONITOR_TYPE}.keyword`,
size: 10000,
},
},
tags: {
terms: {
field: `${monitorAttributes}.${ConfigKey.TAGS}`,
field: `${syntheticsMonitorAttributes}.${ConfigKey.TAGS}`,
size: 10000,
},
},
locations: {
terms: {
field: `${monitorAttributes}.${ConfigKey.LOCATIONS}.id`,
field: `${syntheticsMonitorAttributes}.${ConfigKey.LOCATIONS}.id`,
size: 10000,
},
},
projects: {
terms: {
field: `${monitorAttributes}.${ConfigKey.PROJECT_ID}`,
field: `${syntheticsMonitorAttributes}.${ConfigKey.PROJECT_ID}`,
size: 10000,
},
},
schedules: {
terms: {
field: `${monitorAttributes}.${ConfigKey.SCHEDULE}.number`,
field: `${syntheticsMonitorAttributes}.${ConfigKey.SCHEDULE}.number`,
size: 10000,
},
},

View file

@ -20,7 +20,7 @@ import { getConnectorTypesRoute } from './default_alerts/get_connector_types';
import { getActionConnectorsRoute } from './default_alerts/get_action_connectors';
import { SyntheticsRestApiRouteFactory } from './types';
import { getSyntheticsCertsRoute } from './certs/get_certificates';
import { getSyntheticsSuggestionsRoute } from './suggestions/route';
import { getSyntheticsSuggestionsRoute } from './suggestions/suggestions_route';
import { getAgentPoliciesRoute } from './settings/private_locations/get_agent_policies';
import { inspectSyntheticsMonitorRoute } from './monitor_cruds/inspect_monitor';
import { deletePackagePolicyRoute } from './monitor_cruds/delete_integration';

View file

@ -7,6 +7,10 @@
import { schema } from '@kbn/config-schema';
import { SavedObjectsErrorHelpers } from '@kbn/core/server';
import { i18n } from '@kbn/i18n';
import {
legacySyntheticsMonitorTypeSingle,
syntheticsMonitorSavedObjectType,
} from '../../../common/types/saved_objects';
import { validatePermissions } from './edit_monitor';
import {
InvalidLocationError,
@ -34,13 +38,25 @@ export const addSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () => ({
defaultValue: false,
})
),
// primarily used for testing purposes, to specify the type of saved object
savedObjectType: schema.maybe(
schema.oneOf(
[
schema.literal(syntheticsMonitorSavedObjectType),
schema.literal(legacySyntheticsMonitorTypeSingle),
],
{
defaultValue: syntheticsMonitorSavedObjectType,
}
)
),
}),
},
},
handler: async (routeContext): Promise<any> => {
const { request, response, server } = routeContext;
const { request, response, server, spaceId } = routeContext;
// usually id is auto generated, but this is useful for testing
const { id, internal } = request.query;
const { id, internal, savedObjectType } = request.query;
const addMonitorAPI = new AddEditMonitorAPI(routeContext);
@ -80,7 +96,7 @@ export const addSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () => ({
request.body as CreateMonitorPayLoad
);
const validationResult = validateMonitor(monitorWithDefaults);
const validationResult = validateMonitor(monitorWithDefaults, spaceId);
if (!validationResult.valid || !validationResult.decodedMonitor) {
const { reason: message, details } = validationResult;
@ -91,7 +107,12 @@ export const addSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () => ({
const normalizedMonitor = validationResult.decodedMonitor;
const err = await validatePermissions(routeContext, normalizedMonitor.locations);
// Parallelize permission and unique name validation
const [err, nameError] = await Promise.all([
validatePermissions(routeContext, normalizedMonitor.locations),
addMonitorAPI.validateUniqueMonitorName(normalizedMonitor.name),
]);
if (err) {
return response.forbidden({
body: {
@ -99,7 +120,6 @@ export const addSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () => ({
},
});
}
const nameError = await addMonitorAPI.validateUniqueMonitorName(normalizedMonitor.name);
if (nameError) {
return response.badRequest({
body: { message: nameError, attributes: { details: nameError } },
@ -109,6 +129,7 @@ export const addSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () => ({
const { errors, newMonitor } = await addMonitorAPI.syncNewMonitor({
id,
normalizedMonitor,
savedObjectType,
});
if (errors && errors.length > 0) {

View file

@ -8,6 +8,7 @@
import { AddEditMonitorAPI } from './add_monitor_api';
import { SyntheticsMonitorClient } from '../../../synthetics_service/synthetics_monitor/synthetics_monitor_client';
import { SyntheticsService } from '../../../synthetics_service/synthetics_service';
import { syntheticsMonitorAttributes } from '../../../../common/types/saved_objects';
describe('AddNewMonitorsPublicAPI', () => {
it('should normalize schedule', async function () {
@ -109,6 +110,7 @@ describe('AddNewMonitorsPublicAPI', () => {
urls: '',
labels: {},
maintenance_windows: [],
spaces: [],
});
});
it('should normalize icmp', async () => {
@ -147,6 +149,7 @@ describe('AddNewMonitorsPublicAPI', () => {
wait: '1',
labels: {},
maintenance_windows: [],
spaces: [],
});
});
it('should normalize http', async () => {
@ -207,6 +210,7 @@ describe('AddNewMonitorsPublicAPI', () => {
username: '',
labels: {},
maintenance_windows: [],
spaces: [],
});
});
it('should normalize browser', async () => {
@ -263,7 +267,61 @@ describe('AddNewMonitorsPublicAPI', () => {
urls: '',
labels: {},
maintenance_windows: [],
spaces: [],
});
});
});
describe('validateUniqueMonitorName', () => {
it('should return an error message if the monitor name already exists', async () => {
const api = new AddEditMonitorAPI({
monitorConfigRepository: {
find: async () => ({ total: 1 }),
},
} as any);
const result = await api.validateUniqueMonitorName('test-monitor');
expect(result).toBe('Monitor name must be unique, "test-monitor" already exists.');
});
it('should not return an error message if the monitor name is unique', async () => {
const api = new AddEditMonitorAPI({
monitorConfigRepository: {
find: async () => ({ total: 0 }),
},
} as any);
const result = await api.validateUniqueMonitorName('unique-monitor');
expect(result).toBeUndefined();
});
it('should not return an error message if the monitor name is the same as the one being edited', async () => {
let receivedFilter: string | undefined;
const api = new AddEditMonitorAPI({
monitorConfigRepository: {
find: async (options: { filter: string }) => {
receivedFilter = options.filter;
return { total: 0 };
},
},
} as any);
const result = await api.validateUniqueMonitorName('test-monitor', 'monitor-id');
expect(result).toBeUndefined();
expect(receivedFilter).toBe(
`${syntheticsMonitorAttributes}.name.keyword:"test-monitor" and not (${syntheticsMonitorAttributes}.config_id: monitor-id)`
);
});
it('should return an error message if the monitor name is used by another monitor when editing', async () => {
const api = new AddEditMonitorAPI({
monitorConfigRepository: {
find: async () => ({ total: 1 }),
},
} as any);
const result = await api.validateUniqueMonitorName('test-monitor', 'monitor-id');
expect(result).toBe('Monitor name must be unique, "test-monitor" already exists.');
});
});
});

View file

@ -9,11 +9,15 @@ import { v4 as uuidV4 } from 'uuid';
import { SavedObject } from '@kbn/core-saved-objects-common/src/server_types';
import { isValidNamespace } from '@kbn/fleet-plugin/common';
import { i18n } from '@kbn/i18n';
import {
legacySyntheticsMonitorTypeSingle,
syntheticsMonitorAttributes,
syntheticsMonitorSavedObjectType,
} from '../../../../common/types/saved_objects';
import { DeleteMonitorAPI } from '../services/delete_monitor_api';
import { parseMonitorLocations } from './utils';
import { MonitorValidationError } from '../monitor_validation';
import { getSavedObjectKqlFilter } from '../../common';
import { monitorAttributes } from '../../../../common/types/saved_objects';
import { PrivateLocationAttributes } from '../../../runtime_types/private_locations';
import { ConfigKey } from '../../../../common/constants/monitor_management';
import {
@ -57,9 +61,11 @@ export class AddEditMonitorAPI {
async syncNewMonitor({
id,
normalizedMonitor,
savedObjectType,
}: {
id?: string;
normalizedMonitor: SyntheticsMonitor;
savedObjectType?: string;
}) {
const { server, syntheticsMonitorClient, spaceId } = this.routeContext;
const newMonitorId = id ?? uuidV4();
@ -74,6 +80,8 @@ export class AddEditMonitorAPI {
const newMonitorPromise = this.routeContext.monitorConfigRepository.create({
normalizedMonitor: monitorWithNamespace,
id: newMonitorId,
spaceId,
savedObjectType,
});
const syncErrorsPromise = syntheticsMonitorClient.addMonitors(
@ -205,7 +213,9 @@ export class AddEditMonitorAPI {
const kqlFilter = getSavedObjectKqlFilter({ field: 'name.keyword', values: name });
const { total } = await monitorConfigRepository.find({
perPage: 0,
filter: id ? `${kqlFilter} and not (${monitorAttributes}.config_id: ${id})` : kqlFilter,
filter: id
? `${kqlFilter} and not (${syntheticsMonitorAttributes}.config_id: ${id})`
: kqlFilter,
});
if (total > 0) {
@ -217,7 +227,12 @@ export class AddEditMonitorAPI {
}
initDefaultAlerts(name: string) {
const { server, savedObjectsClient, context } = this.routeContext;
const { server, savedObjectsClient, context, request } = this.routeContext;
const { gettingStarted } = request.query;
if (!gettingStarted) {
return;
}
try {
// we do this async, so we don't block the user, error handling will be done on the UI via separate api
const defaultAlertService = new DefaultAlertService(context, server, savedObjectsClient);
@ -305,7 +320,10 @@ export class AddEditMonitorAPI {
try {
const encryptedMonitor = await monitorConfigRepository.get(newMonitorId);
if (encryptedMonitor) {
await monitorConfigRepository.delete(newMonitorId);
await monitorConfigRepository.bulkDelete([
{ id: newMonitorId, type: syntheticsMonitorSavedObjectType },
{ id: newMonitorId, type: legacySyntheticsMonitorTypeSingle },
]);
const deleteMonitorAPI = new DeleteMonitorAPI(this.routeContext);
await deleteMonitorAPI.execute({

View file

@ -39,7 +39,8 @@ export const syncNewMonitorBulk = async ({
privateLocations: SyntheticsPrivateLocations;
spaceId: string;
}) => {
const { server, syntheticsMonitorClient, monitorConfigRepository } = routeContext;
const { server, syntheticsMonitorClient, monitorConfigRepository, request } = routeContext;
const { query } = request;
let newMonitors: CreatedMonitors | null = null;
const monitorsToCreate = normalizedMonitors.map((monitor) => {
@ -59,6 +60,7 @@ export const syncNewMonitorBulk = async ({
const [createdMonitors, [policiesResult, syncErrors]] = await Promise.all([
monitorConfigRepository.createBulk({
monitors: monitorsToCreate,
savedObjectType: query.savedObjectType,
}),
syntheticsMonitorClient.addMonitors(monitorsToCreate, privateLocations, spaceId),
]);

View file

@ -81,6 +81,7 @@ export const syncEditedMonitorBulk = async ({
[ConfigKey.MONITOR_QUERY_ID]:
monitorWithRevision[ConfigKey.CUSTOM_HEARTBEAT_ID] || decryptedPreviousMonitor.id,
} as unknown as MonitorFields,
soType: decryptedPreviousMonitor.type,
}));
const [editedMonitorSavedObjects, editSyncResponse] = await Promise.all([
monitorConfigRepository.bulkUpdate({
@ -140,6 +141,7 @@ export const rollbackCompletely = async ({
monitors: monitorsToUpdate.map(({ decryptedPreviousMonitor }) => ({
id: decryptedPreviousMonitor.id,
attributes: decryptedPreviousMonitor.attributes as unknown as MonitorFields,
soType: decryptedPreviousMonitor.type,
})),
});
} catch (error) {
@ -187,6 +189,7 @@ export const rollbackFailedUpdates = async ({
.map(({ decryptedPreviousMonitor }) => ({
id: decryptedPreviousMonitor.id,
attributes: decryptedPreviousMonitor.attributes as unknown as MonitorFields,
soType: decryptedPreviousMonitor.type,
}));
if (monitorsToRevert.length > 0) {

View file

@ -5,18 +5,14 @@
* 2.0.
*/
import { loggerMock } from '@kbn/logging-mocks';
import { syncEditedMonitor } from './edit_monitor';
import { SavedObject, SavedObjectsClientContract, KibanaRequest } from '@kbn/core/server';
import { SavedObject } from '@kbn/core/server';
import {
EncryptedSyntheticsMonitorAttributes,
SyntheticsMonitor,
SyntheticsMonitorWithSecretsAttributes,
} from '../../../common/runtime_types';
import { SyntheticsService } from '../../synthetics_service/synthetics_service';
import { SyntheticsMonitorClient } from '../../synthetics_service/synthetics_monitor/synthetics_monitor_client';
import { mockEncryptedSO } from '../../synthetics_service/utils/mocks';
import { SyntheticsServerSetup } from '../../types';
import { getRouteContextMock } from '../../mocks/route_context_mock';
jest.mock('../telemetry/monitor_upgrade_sender', () => ({
sendTelemetryEvents: jest.fn(),
@ -24,43 +20,6 @@ jest.mock('../telemetry/monitor_upgrade_sender', () => ({
}));
describe('syncEditedMonitor', () => {
const logger = loggerMock.create();
const serverMock: SyntheticsServerSetup = {
syntheticsEsClient: { search: jest.fn() },
stackVersion: null,
authSavedObjectsClient: {
bulkUpdate: jest.fn(),
get: jest.fn(),
update: jest.fn(),
createPointInTimeFinder: jest.fn().mockImplementation(({ perPage, type: soType }) => ({
close: jest.fn(async () => {}),
find: jest.fn().mockReturnValue({
async *[Symbol.asyncIterator]() {
yield {
saved_objects: [],
};
},
}),
})),
},
logger,
config: {
service: {
username: 'dev',
password: '12345',
},
},
fleet: {
packagePolicyService: {
get: jest.fn().mockReturnValue({}),
getByIDs: jest.fn().mockReturnValue([]),
buildPackagePolicyFromPackage: jest.fn().mockReturnValue({}),
},
},
encryptedSavedObjects: mockEncryptedSO(),
} as unknown as SyntheticsServerSetup;
const editedMonitor = {
type: 'http',
enabled: true,
@ -91,10 +50,7 @@ describe('syncEditedMonitor', () => {
references: [],
} as SavedObject<EncryptedSyntheticsMonitorAttributes>;
const syntheticsService = new SyntheticsService(serverMock);
const syntheticsMonitorClient = new SyntheticsMonitorClient(syntheticsService, serverMock);
const { routeContext, syntheticsService, serverMock } = getRouteContextMock();
syntheticsService.editConfig = jest.fn();
syntheticsService.getMaintenanceWindows = jest.fn();
@ -103,13 +59,7 @@ describe('syncEditedMonitor', () => {
normalizedMonitor: editedMonitor,
decryptedPreviousMonitor:
previousMonitor as unknown as SavedObject<SyntheticsMonitorWithSecretsAttributes>,
routeContext: {
syntheticsMonitorClient,
server: serverMock,
request: {} as unknown as KibanaRequest,
savedObjectsClient:
serverMock.authSavedObjectsClient as unknown as SavedObjectsClientContract,
} as any,
routeContext,
spaceId: 'test-space',
});

View file

@ -8,6 +8,7 @@ import { schema } from '@kbn/config-schema';
import { SavedObjectsUpdateResponse, SavedObject } from '@kbn/core/server';
import { SavedObjectsErrorHelpers } from '@kbn/core/server';
import { isEmpty } from 'lodash';
import { syntheticsMonitorSavedObjectType } from '../../../common/types/saved_objects';
import { invalidOriginError } from './add_monitor';
import {
InvalidLocationError,
@ -15,11 +16,9 @@ import {
} from '../../synthetics_service/project_monitor/normalizers/common_fields';
import { AddEditMonitorAPI, CreateMonitorPayLoad } from './add_monitor/add_monitor_api';
import { ELASTIC_MANAGED_LOCATIONS_DISABLED } from './project_monitor/add_monitor_project';
import { getDecryptedMonitor } from '../../saved_objects/synthetics_monitor';
import { getPrivateLocations } from '../../synthetics_service/get_private_locations';
import { mergeSourceMonitor } from './formatters/saved_object_to_monitor';
import { RouteContext, SyntheticsRestApiRouteFactory } from '../types';
import { syntheticsMonitorType } from '../../../common/types/saved_objects';
import {
MonitorFields,
EncryptedSyntheticsMonitorAttributes,
@ -35,7 +34,7 @@ import {
sendTelemetryEvents,
formatTelemetryUpdateEvent,
} from '../telemetry/monitor_upgrade_sender';
import { formatSecrets, normalizeSecrets } from '../../synthetics_service/utils/secrets';
import { formatSecrets } from '../../synthetics_service/utils/secrets';
import { mapSavedObjectToMonitor } from './formatters/saved_object_to_monitor';
// Simplify return promise type and type it with runtime_types
@ -59,7 +58,7 @@ export const editSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () => (
},
},
handler: async (routeContext): Promise<any> => {
const { request, response, spaceId, server } = routeContext;
const { request, response, spaceId, server, monitorConfigRepository } = routeContext;
const { logger } = server;
const monitor = request.body as SyntheticsMonitor;
const reqQuery = request.query as { internal?: boolean };
@ -90,8 +89,9 @@ export const editSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () => (
/* Decrypting the previous monitor before editing ensures that all existing fields remain
* on the object, even in flows where decryption does not take place, such as the enabled tab
* on the monitor list table. We do not decrypt monitors in bulk for the monitor list table */
const previousMonitor = await getDecryptedMonitor(server, monitorId, spaceId);
const normalizedPreviousMonitor = normalizeSecrets(previousMonitor).attributes;
const { decryptedMonitor: decryptedMonitorPrevMonitor, normalizedMonitor: previousMonitor } =
await monitorConfigRepository.getDecrypted(monitorId, spaceId);
const normalizedPreviousMonitor = previousMonitor.attributes;
if (normalizedPreviousMonitor.origin !== 'ui' && !reqQuery.internal) {
return response.badRequest(getInvalidOriginError(monitor));
@ -122,7 +122,7 @@ export const editSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () => (
previousMonitor.attributes.locations
);
const validationResult = validateMonitor(editedMonitor as MonitorFields);
const validationResult = validateMonitor(editedMonitor as MonitorFields, spaceId);
if (!validationResult.valid || !validationResult.decodedMonitor) {
const { reason: message, details, payload } = validationResult;
@ -153,7 +153,7 @@ export const editSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () => (
editedMonitor: editedMonitorSavedObject,
} = await syncEditedMonitor({
routeContext,
decryptedPreviousMonitor: previousMonitor,
decryptedPreviousMonitor: decryptedMonitorPrevMonitor,
normalizedMonitor: monitorWithRevision,
spaceId,
});
@ -162,7 +162,7 @@ export const editSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () => (
await rollbackUpdate({
routeContext,
configId: monitorId,
attributes: previousMonitor.attributes,
attributes: decryptedMonitorPrevMonitor.attributes,
});
throw hasError?.error;
}
@ -219,7 +219,11 @@ const rollbackUpdate = async ({
}) => {
const { savedObjectsClient, server } = routeContext;
try {
await savedObjectsClient.update<MonitorFields>(syntheticsMonitorType, configId, attributes);
await savedObjectsClient.update<MonitorFields>(
syntheticsMonitorSavedObjectType,
configId,
attributes
);
} catch (error) {
server.logger.error(
`Unable to rollback edit for Synthetics monitor with id ${configId}, Error: ${error.message}`,
@ -241,20 +245,22 @@ export const syncEditedMonitor = async ({
routeContext: RouteContext;
spaceId: string;
}) => {
const { server, savedObjectsClient, syntheticsMonitorClient } = routeContext;
const { server, savedObjectsClient, syntheticsMonitorClient, monitorConfigRepository } =
routeContext;
try {
const monitorWithId = {
...normalizedMonitor,
[ConfigKey.MONITOR_QUERY_ID]:
normalizedMonitor[ConfigKey.CUSTOM_HEARTBEAT_ID] || decryptedPreviousMonitor.id,
[ConfigKey.CONFIG_ID]: decryptedPreviousMonitor.id,
[ConfigKey.KIBANA_SPACES]:
normalizedMonitor[ConfigKey.KIBANA_SPACES] || decryptedPreviousMonitor.namespaces,
};
const formattedMonitor = formatSecrets(monitorWithId);
const editedSOPromise = savedObjectsClient.update<MonitorFields>(
syntheticsMonitorType,
const editedSOPromise = monitorConfigRepository.update(
decryptedPreviousMonitor.id,
formattedMonitor
formattedMonitor,
decryptedPreviousMonitor
);
const allPrivateLocations = await getPrivateLocations(savedObjectsClient);

View file

@ -7,7 +7,6 @@
import { schema } from '@kbn/config-schema';
import { SavedObjectsErrorHelpers } from '@kbn/core/server';
import { SyntheticsRestApiRouteFactory } from '../types';
import { syntheticsMonitorType } from '../../../common/types/saved_objects';
import { isStatusEnabled } from '../../../common/runtime_types/monitor_management/alert_config';
import { ConfigKey, EncryptedSyntheticsMonitorAttributes } from '../../../common/runtime_types';
import { SYNTHETICS_API_URLS } from '../../../common/constants';
@ -35,8 +34,7 @@ export const getSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () => ({
handler: async ({
request,
response,
server: { encryptedSavedObjects, coreStart },
savedObjectsClient,
server: { coreStart },
spaceId,
monitorConfigRepository,
}): Promise<any> => {
@ -54,17 +52,20 @@ export const getSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () => ({
if (Boolean(canSave)) {
// only user with write permissions can decrypt the monitor
const monitor = await monitorConfigRepository.getDecrypted(monitorId, spaceId);
return { ...mapSavedObjectToMonitor({ monitor, internal }), spaceId };
return {
...mapSavedObjectToMonitor({ monitor: monitor.normalizedMonitor, internal }),
spaceId,
spaces: monitor.decryptedMonitor.namespaces,
};
} else {
const monObj = await monitorConfigRepository.get(monitorId);
return {
...mapSavedObjectToMonitor({
monitor: await savedObjectsClient.get<EncryptedSyntheticsMonitorAttributes>(
syntheticsMonitorType,
monitorId
),
monitor: monObj,
internal,
}),
spaceId,
spaces: monObj.namespaces,
};
}
} catch (getErr) {

View file

@ -4,10 +4,18 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { EncryptedSyntheticsMonitorAttributes } from '../../../common/runtime_types';
import { mapSavedObjectToMonitor } from './formatters/saved_object_to_monitor';
import { SyntheticsRestApiRouteFactory } from '../types';
import { SYNTHETICS_API_URLS } from '../../../common/constants';
import { getMonitors, isMonitorsQueryFiltered, QuerySchema } from '../common';
import {
getMonitorFilters,
isMonitorsQueryFiltered,
MonitorsQuery,
parseMappingKey,
QuerySchema,
SEARCH_FIELDS,
} from '../common';
export const getAllSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () => ({
method: 'GET',
@ -28,9 +36,22 @@ export const getAllSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () =>
});
}
};
const queryParams = routeContext.request.query as MonitorsQuery;
const { filtersStr } = await getMonitorFilters(routeContext);
const [queryResultSavedObjects, totalCount] = await Promise.all([
getMonitors(routeContext),
monitorConfigRepository.find<EncryptedSyntheticsMonitorAttributes>({
perPage: queryParams.perPage ?? 50,
page: queryParams.page ?? 1,
sortField: parseMappingKey(queryParams.sortField),
sortOrder: queryParams.sortOrder,
searchFields: SEARCH_FIELDS,
search: queryParams.query,
filter: filtersStr,
searchAfter: queryParams.searchAfter,
...(queryParams.showFromAllSpaces && { namespaces: ['*'] }),
}),
totalCountQuery(),
]);
@ -46,8 +67,9 @@ export const getAllSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () =>
internal: request.query?.internal,
});
return {
spaceId: monitor.namespaces?.[0],
...mon,
spaceId: monitor.namespaces?.[0],
spaces: monitor.namespaces ?? [],
};
}),
absoluteTotal,

View file

@ -39,7 +39,7 @@ export const inspectSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () =
...monitor,
};
const validationResult = validateMonitor(monitorWithDefaults as MonitorFields);
const validationResult = validateMonitor(monitorWithDefaults as MonitorFields, spaceId);
if (!validationResult.valid || !validationResult.decodedMonitor) {
const { reason: message, details, payload } = validationResult;

View file

@ -204,7 +204,7 @@ describe('validateMonitor', () => {
},
locations: ['somewhere'],
} as unknown as MonitorFields;
const result = validateMonitor(testMonitor);
const result = validateMonitor(testMonitor, 'default');
expect(result).toMatchObject({
valid: false,
reason: 'Monitor type is invalid',
@ -221,7 +221,7 @@ describe('validateMonitor', () => {
},
locations: ['somewhere'],
} as unknown as MonitorFields;
const result = validateMonitor(monitor);
const result = validateMonitor(monitor, 'default');
expect(result).toMatchObject({
valid: false,
reason: 'Monitor type is invalid',
@ -230,13 +230,16 @@ describe('validateMonitor', () => {
});
it(`when schedule is not valid`, () => {
const result = validateMonitor({
const result = validateMonitor(
{
...testICMPFields,
schedule: {
number: '4',
unit: ScheduleUnit.MINUTES,
},
} as unknown as MonitorFields);
} as unknown as MonitorFields,
'default'
);
expect(result).toMatchObject({
valid: false,
reason: 'Monitor schedule is invalid',
@ -246,10 +249,13 @@ describe('validateMonitor', () => {
});
it(`when timeout is not valid`, () => {
const result = validateMonitor({
const result = validateMonitor(
{
...testICMPFields,
timeout: '3m',
} as unknown as MonitorFields);
} as unknown as MonitorFields,
'default'
);
expect(result).toMatchObject({
valid: false,
reason: 'Monitor is not a valid monitor of type icmp',
@ -258,10 +264,13 @@ describe('validateMonitor', () => {
});
it(`when location is not valid`, () => {
const result = validateMonitor({
const result = validateMonitor(
{
...testICMPFields,
locations: ['invalid-location'],
} as unknown as MonitorFields);
} as unknown as MonitorFields,
'default'
);
expect(result).toMatchObject({
valid: false,
reason: 'Monitor is not a valid monitor of type icmp',
@ -273,7 +282,7 @@ describe('validateMonitor', () => {
describe('should validate', () => {
it('when payload is a correct ICMP monitor', () => {
const testMonitor = testICMPFields as MonitorFields;
const result = validateMonitor(testMonitor);
const result = validateMonitor(testMonitor, 'default');
expect(result).toMatchObject({
valid: true,
reason: '',
@ -284,7 +293,7 @@ describe('validateMonitor', () => {
it('when payload is a correct TCP monitor', () => {
const testMonitor = testTCPFields as MonitorFields;
const result = validateMonitor(testMonitor);
const result = validateMonitor(testMonitor, 'default');
expect(result).toMatchObject({
valid: true,
reason: '',
@ -296,7 +305,7 @@ describe('validateMonitor', () => {
it('when payload is a correct HTTP monitor', () => {
const testMonitor = testHTTPFields as MonitorFields;
const result = validateMonitor(testMonitor);
const result = validateMonitor(testMonitor, 'default');
expect(result).toMatchObject({
valid: true,
reason: '',
@ -307,7 +316,7 @@ describe('validateMonitor', () => {
it('when payload is not a correct Browser monitor', () => {
const testMonitor = testBrowserFields as MonitorFields;
const result = validateMonitor(testMonitor);
const result = validateMonitor(testMonitor, 'default');
expect(result).toMatchObject({
valid: false,
details: 'source.inline.script: Script is required for browser monitor.',
@ -321,7 +330,7 @@ describe('validateMonitor', () => {
...testBrowserFields,
[ConfigKey.SOURCE_INLINE]: 'journey()',
} as MonitorFields;
const result = validateMonitor(testMonitor);
const result = validateMonitor(testMonitor, 'default');
expect(result).toMatchObject({
valid: false,
reason: 'Monitor is not a valid monitor of type browser',
@ -336,7 +345,7 @@ describe('validateMonitor', () => {
...testBrowserFields,
[ConfigKey.SOURCE_INLINE]: 'step()',
} as MonitorFields;
const result = validateMonitor(testMonitor);
const result = validateMonitor(testMonitor, 'default');
expect(result).toMatchObject({
valid: true,
reason: '',
@ -355,7 +364,7 @@ describe('validateMonitor', () => {
} as unknown as Partial<ICMPSimpleFields>),
} as MonitorFields;
const result = validateMonitor(testMonitor);
const result = validateMonitor(testMonitor, 'default');
expect(result.details).toEqual(expect.stringContaining('Invalid value'));
expect(result.details).toEqual(expect.stringContaining(ConfigKey.HOSTS));
@ -374,7 +383,7 @@ describe('validateMonitor', () => {
} as unknown as Partial<TCPFields>),
} as MonitorFields;
const result = validateMonitor(testMonitor);
const result = validateMonitor(testMonitor, 'default');
expect(result.details).toEqual(
expect.stringContaining('Invalid field "host", must be a non-empty string.')
@ -394,7 +403,7 @@ describe('validateMonitor', () => {
} as unknown as Partial<HTTPFields>),
} as MonitorFields;
const result = validateMonitor(testMonitor);
const result = validateMonitor(testMonitor, 'default');
expect(result.details).toEqual('Invalid field "url", must be a non-empty string.');
expect(result).toMatchObject({
@ -412,7 +421,7 @@ describe('validateMonitor', () => {
} as unknown as Partial<BrowserFields>),
} as MonitorFields;
const result = validateMonitor(testMonitor);
const result = validateMonitor(testMonitor, 'default');
expect(result.details).toEqual(
expect.stringContaining('source.inline.script: Inline script must be a non-empty string')
@ -436,7 +445,7 @@ describe('validateMonitor', () => {
} as unknown as Partial<TCPFields>),
} as MonitorFields;
const result = validateMonitor(testMonitor);
const result = validateMonitor(testMonitor, 'default');
expect(result).toMatchObject({
valid: true,
@ -451,7 +460,7 @@ describe('validateMonitor', () => {
it('when parsed from serialized JSON', () => {
const testMonitor = getJsonPayload() as MonitorFields;
const result = validateMonitor(testMonitor);
const result = validateMonitor(testMonitor, 'default');
expect(result).toMatchObject({
valid: true,
@ -463,10 +472,13 @@ describe('validateMonitor', () => {
it('when parsed from serialized JSON for alert', () => {
const testMonitor = getJsonPayload() as MonitorFields;
const result = validateMonitor({
const result = validateMonitor(
{
...testMonitor,
alert: {},
});
},
'default'
);
expect(result).toMatchObject({
valid: false,
@ -478,14 +490,17 @@ describe('validateMonitor', () => {
it('when parsed from serialized JSON for alert invalid key', () => {
const testMonitor = getJsonPayload() as MonitorFields;
const result = validateMonitor({
const result = validateMonitor(
{
...testMonitor,
alert: {
// @ts-ignore
invalidKey: 'invalid',
enabled: true,
},
});
},
'default'
);
expect(result).toMatchObject({
valid: false,

View file

@ -9,7 +9,7 @@ import { i18n } from '@kbn/i18n';
import { isLeft } from 'fp-ts/Either';
import { formatErrors } from '@kbn/securitysolution-io-ts-utils';
import { omit } from 'lodash';
import { omit, isEmpty } from 'lodash';
import { schema } from '@kbn/config-schema';
import { AlertConfigSchema } from '../../../common/runtime_types/monitor_management/alert_config_schema';
import { CreateMonitorPayLoad } from './add_monitor/add_monitor_api';
@ -69,9 +69,11 @@ export class MonitorValidationError extends Error {
/**
* 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.
* @param spaceId
*/
export function validateMonitor(monitorFields: MonitorFields): ValidationResult {
const { [ConfigKey.MONITOR_TYPE]: monitorType } = monitorFields;
export function validateMonitor(monitorFields: MonitorFields, spaceId: string): ValidationResult {
const { [ConfigKey.MONITOR_TYPE]: monitorType, [ConfigKey.KIBANA_SPACES]: kSpaces } =
monitorFields;
if (monitorType !== MonitorTypeEnum.BROWSER && !monitorFields.name) {
monitorFields.name = monitorFields.urls || monitorFields.hosts;
@ -159,6 +161,21 @@ export function validateMonitor(monitorFields: MonitorFields): ValidationResult
}
}
if (spaceId && !isEmpty(kSpaces)) {
// we throw error if kSpaces is not empty and spaceId is not present
if (kSpaces && !kSpaces.includes(spaceId) && !kSpaces.includes('*')) {
return {
valid: false,
reason: i18n.translate('xpack.synthetics.createMonitor.validation.invalidSpace', {
defaultMessage:
'Invalid space ID provided in monitor configuration. It should always include the current space ID.',
}),
details: '',
payload: monitorFields,
};
}
}
return {
valid: true,
reason: '',
@ -230,6 +247,10 @@ export const normalizeAPIConfig = (monitor: CreateMonitorPayLoad) => {
let unsupportedKeys = Object.keys(rawConfig).filter((key) => !supportedKeys.includes(key));
const result = omit(rawConfig, unsupportedKeys);
let kSpaces = rawConfig[ConfigKey.KIBANA_SPACES] as string[];
if (kSpaces?.includes('*')) {
kSpaces = ['*'];
}
const formattedConfig = {
...result,
@ -237,6 +258,7 @@ export const normalizeAPIConfig = (monitor: CreateMonitorPayLoad) => {
private_locations: _privateLocations,
retest_on_failure: _retestOnFailure,
custom_heartbeat_id: _customHeartbeatId,
...(kSpaces ? { [ConfigKey.KIBANA_SPACES]: kSpaces } : {}),
} as CreateMonitorPayLoad;
const requestBodyCheck = formattedConfig[ConfigKey.REQUEST_BODY_CHECK];

View file

@ -7,6 +7,10 @@
import { schema } from '@kbn/config-schema';
import { i18n } from '@kbn/i18n';
import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common';
import {
legacySyntheticsMonitorTypeSingle,
syntheticsMonitorSavedObjectType,
} from '../../../../common/types/saved_objects';
import { validateSpaceId } from '../services/validate_space_id';
import { RouteContext, SyntheticsRestApiRouteFactory } from '../../types';
import { ProjectMonitor } from '../../../../common/runtime_types';
@ -20,6 +24,20 @@ export const addSyntheticsProjectMonitorRoute: SyntheticsRestApiRouteFactory = (
method: 'PUT',
path: SYNTHETICS_API_URLS.SYNTHETICS_MONITORS_PROJECT_UPDATE,
validate: {
query: schema.object({
// primarily used for testing purposes, to specify the type of saved object
savedObjectType: schema.maybe(
schema.oneOf(
[
schema.literal(syntheticsMonitorSavedObjectType),
schema.literal(legacySyntheticsMonitorTypeSingle),
],
{
defaultValue: syntheticsMonitorSavedObjectType,
}
)
),
}),
params: schema.object({
projectName: schema.string(),
}),

View file

@ -6,12 +6,12 @@
*/
import { schema } from '@kbn/config-schema';
import { i18n } from '@kbn/i18n';
import { syntheticsMonitorAttributes } from '../../../../common/types/saved_objects';
import { DeleteMonitorAPI } from '../services/delete_monitor_api';
import { SyntheticsRestApiRouteFactory } from '../../types';
import { monitorAttributes } from '../../../../common/types/saved_objects';
import { ConfigKey } from '../../../../common/runtime_types';
import { ConfigKey, EncryptedSyntheticsMonitorAttributes } from '../../../../common/runtime_types';
import { SYNTHETICS_API_URLS } from '../../../../common/constants';
import { getMonitors, getSavedObjectKqlFilter } from '../../common';
import { getSavedObjectKqlFilter } from '../../common';
import { validateSpaceId } from '../services/validate_space_id';
export const deleteSyntheticsMonitorProjectRoute: SyntheticsRestApiRouteFactory = () => ({
@ -26,7 +26,7 @@ export const deleteSyntheticsMonitorProjectRoute: SyntheticsRestApiRouteFactory
}),
},
handler: async (routeContext): Promise<any> => {
const { request, response } = routeContext;
const { request, response, monitorConfigRepository } = routeContext;
const { projectName } = request.params;
const { monitors: monitorsToDelete } = request.body;
const decodedProjectName = decodeURI(projectName);
@ -40,23 +40,19 @@ export const deleteSyntheticsMonitorProjectRoute: SyntheticsRestApiRouteFactory
await validateSpaceId(routeContext);
const deleteFilter = `${monitorAttributes}.${
const deleteFilter = `${syntheticsMonitorAttributes}.${
ConfigKey.PROJECT_ID
}: "${decodedProjectName}" AND ${getSavedObjectKqlFilter({
field: 'journey_id',
values: monitorsToDelete.map((id: string) => `${id}`),
})}`;
const { saved_objects: monitors } = await getMonitors(
{
...routeContext,
request: {
...request,
query: { ...request.query, filter: deleteFilter, perPage: 500 },
},
},
{ fields: [] }
);
const { saved_objects: monitors } =
await monitorConfigRepository.find<EncryptedSyntheticsMonitorAttributes>({
perPage: 500,
filter: deleteFilter,
fields: [],
});
const deleteMonitorAPI = new DeleteMonitorAPI(routeContext);

View file

@ -5,11 +5,11 @@
* 2.0.
*/
import { schema } from '@kbn/config-schema';
import { syntheticsMonitorSavedObjectType } from '../../../../common/types/saved_objects';
import { SyntheticsRestApiRouteFactory } from '../../types';
import { syntheticsMonitorType } from '../../../../common/types/saved_objects';
import { ConfigKey } from '../../../../common/runtime_types';
import { ConfigKey, EncryptedSyntheticsMonitorAttributes } from '../../../../common/runtime_types';
import { SYNTHETICS_API_URLS } from '../../../../common/constants';
import { getMonitors } from '../../common';
import { SEARCH_FIELDS } from '../../common';
const querySchema = schema.object({
search_after: schema.maybe(schema.string()),
@ -29,6 +29,7 @@ export const getSyntheticsProjectMonitorsRoute: SyntheticsRestApiRouteFactory =
const {
request,
server: { logger },
monitorConfigRepository,
} = routeContext;
const { projectName } = request.params;
@ -37,25 +38,16 @@ export const getSyntheticsProjectMonitorsRoute: SyntheticsRestApiRouteFactory =
const decodedSearchAfter = searchAfter ? decodeURI(searchAfter) : undefined;
try {
const { saved_objects: monitors, total } = await getMonitors(
{
...routeContext,
request: {
...request,
query: {
...request.query,
filter: `${syntheticsMonitorType}.attributes.${ConfigKey.PROJECT_ID}: "${decodedProjectName}"`,
const { saved_objects: monitors, total } =
await monitorConfigRepository.find<EncryptedSyntheticsMonitorAttributes>({
perPage,
searchFields: SEARCH_FIELDS,
fields: [ConfigKey.JOURNEY_ID, ConfigKey.CONFIG_HASH],
filter: `${syntheticsMonitorSavedObjectType}.attributes.${ConfigKey.PROJECT_ID}: "${decodedProjectName}"`,
sortField: ConfigKey.JOURNEY_ID,
sortOrder: 'asc',
searchAfter: decodedSearchAfter ? [...decodedSearchAfter.split(',')] : undefined,
},
},
},
{
fields: [ConfigKey.JOURNEY_ID, ConfigKey.CONFIG_HASH],
}
);
searchAfter: decodedSearchAfter ? decodedSearchAfter.split(',') : undefined,
});
const projectMonitors = monitors.map((monitor) => ({
journey_id: monitor.attributes[ConfigKey.JOURNEY_ID],
hash: monitor.attributes[ConfigKey.CONFIG_HASH] || '',

View file

@ -7,6 +7,7 @@
import pMap from 'p-map';
import { SavedObject, SavedObjectsErrorHelpers } from '@kbn/core-saved-objects-server';
import { syntheticsMonitorSavedObjectType } from '../../../../common/types/saved_objects';
import { validatePermissions } from '../edit_monitor';
import {
ConfigKey,
@ -14,10 +15,7 @@ import {
MonitorFields,
SyntheticsMonitor,
SyntheticsMonitorWithId,
SyntheticsMonitorWithSecretsAttributes,
} from '../../../../common/runtime_types';
import { syntheticsMonitorType } from '../../../../common/types/saved_objects';
import { normalizeSecrets } from '../../../synthetics_service/utils';
import {
formatTelemetryDeleteEvent,
sendErrorTelemetryEvents,
@ -50,19 +48,11 @@ export class DeleteMonitorAPI {
}
async getMonitorToDelete(monitorId: string) {
const { spaceId, savedObjectsClient, server } = this.routeContext;
const { spaceId, savedObjectsClient, server, monitorConfigRepository } = this.routeContext;
try {
const encryptedSOClient = server.encryptedSavedObjects.getClient();
const { normalizedMonitor } = await monitorConfigRepository.getDecrypted(monitorId, spaceId);
const monitor =
await encryptedSOClient.getDecryptedAsInternalUser<SyntheticsMonitorWithSecretsAttributes>(
syntheticsMonitorType,
monitorId,
{
namespace: spaceId,
}
);
return normalizeSecrets(monitor);
return normalizedMonitor;
} catch (e) {
if (SavedObjectsErrorHelpers.isNotFoundError(e)) {
this.result.push({
@ -83,7 +73,7 @@ export class DeleteMonitorAPI {
stackVersion: server.stackVersion,
});
return await savedObjectsClient.get<EncryptedSyntheticsMonitorAttributes>(
syntheticsMonitorType,
syntheticsMonitorSavedObjectType,
monitorId
);
}
@ -146,7 +136,7 @@ export class DeleteMonitorAPI {
);
const deletePromise = this.routeContext.monitorConfigRepository.bulkDelete(
monitors.map((monitor) => monitor.id)
monitors.map((monitor) => ({ id: monitor.id, type: monitor.type }))
);
const [errors, result] = await Promise.all([deleteSyncPromise, deletePromise]);

View file

@ -8,7 +8,6 @@ import { SavedObjectsFindResult } from '@kbn/core-saved-objects-api-server';
import { EncryptedSyntheticsMonitorAttributes } from '../../../common/runtime_types';
import { getUptimeESMockClient } from '../../queries/test_helpers';
import * as commonLibs from '../common';
import * as allLocationsFn from '../../synthetics_service/get_all_locations';
import { OverviewStatusService, SUMMARIES_PAGE_SIZE } from './overview_status_service';
import times from 'lodash/times';
@ -30,38 +29,15 @@ jest.spyOn(allLocationsFn, 'getAllLocations').mockResolvedValue({
allLocations,
});
jest.mock('../../saved_objects/synthetics_monitor/get_all_monitors', () => ({
...jest.requireActual('../../saved_objects/synthetics_monitor/get_all_monitors'),
jest.mock('../../saved_objects/synthetics_monitor/process_monitors', () => ({
...jest.requireActual('../../saved_objects/synthetics_monitor/process_monitors'),
getAllMonitors: jest.fn(),
}));
jest.spyOn(commonLibs, 'getMonitors').mockResolvedValue({
per_page: 10,
saved_objects: [
{
id: 'mon-1',
attributes: {
enabled: false,
locations: [{ id: 'us-east1' }, { id: 'us-west1' }, { id: 'japan' }],
},
},
{
id: 'mon-2',
attributes: {
enabled: true,
locations: [{ id: 'us-east1' }, { id: 'us-west1' }, { id: 'japan' }],
schedule: {
number: '10',
unit: 'm',
},
},
},
],
} as any);
describe('current status route', () => {
const testMonitors = [
{
namespaces: ['default'],
attributes: {
config_id: 'id1',
id: 'id1',
@ -78,6 +54,7 @@ describe('current status route', () => {
},
},
{
namespaces: ['default'],
attributes: {
id: 'id2',
config_id: 'id2',
@ -187,7 +164,9 @@ describe('current status route', () => {
"name": "test monitor 2",
"projectId": "project-id",
"schedule": "1",
"spaceId": undefined,
"spaces": Array [
"default",
],
"status": "down",
"tags": Array [
"tag-1",
@ -219,7 +198,9 @@ describe('current status route', () => {
"name": "test monitor 1",
"projectId": "project-id",
"schedule": "1",
"spaceId": undefined,
"spaces": Array [
"default",
],
"status": "up",
"tags": Array [
"tag-1",
@ -241,7 +222,9 @@ describe('current status route', () => {
"name": "test monitor 2",
"projectId": "project-id",
"schedule": "1",
"spaceId": undefined,
"spaces": Array [
"default",
],
"status": "up",
"tags": Array [
"tag-1",
@ -352,7 +335,9 @@ describe('current status route', () => {
"name": "test monitor 2",
"projectId": "project-id",
"schedule": "1",
"spaceId": undefined,
"spaces": Array [
"default",
],
"status": "down",
"tags": Array [
"tag-1",
@ -384,7 +369,9 @@ describe('current status route', () => {
"name": "test monitor 1",
"projectId": "project-id",
"schedule": "1",
"spaceId": undefined,
"spaces": Array [
"default",
],
"status": "up",
"tags": Array [
"tag-1",
@ -406,7 +393,9 @@ describe('current status route', () => {
"name": "test monitor 2",
"projectId": "project-id",
"schedule": "1",
"spaceId": undefined,
"spaces": Array [
"default",
],
"status": "up",
"tags": Array [
"tag-1",
@ -467,7 +456,9 @@ describe('current status route', () => {
"name": "test monitor 1",
"projectId": "project-id",
"schedule": "1",
"spaceId": undefined,
"spaces": Array [
"default",
],
"status": "unknown",
"tags": Array [
"tag-1",
@ -489,7 +480,9 @@ describe('current status route', () => {
"name": "test monitor 2",
"projectId": "project-id",
"schedule": "1",
"spaceId": undefined,
"spaces": Array [
"default",
],
"status": "unknown",
"tags": Array [
"tag-1",
@ -511,7 +504,9 @@ describe('current status route', () => {
"name": "test monitor 2",
"projectId": "project-id",
"schedule": "1",
"spaceId": undefined,
"spaces": Array [
"default",
],
"status": "unknown",
"tags": Array [
"tag-1",

View file

@ -10,9 +10,10 @@ import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
import { SavedObjectsFindResult } from '@kbn/core-saved-objects-api-server';
import { isEmpty } from 'lodash';
import { withApmSpan } from '@kbn/apm-data-access-plugin/server/utils/with_apm_span';
import { ALL_SPACES_ID } from '@kbn/security-plugin/common/constants';
import { asMutableArray } from '../../../common/utils/as_mutable_array';
import { getMonitorFilters, OverviewStatusQuery } from '../common';
import { processMonitors } from '../../saved_objects/synthetics_monitor/get_all_monitors';
import { processMonitors } from '../../saved_objects/synthetics_monitor/process_monitors';
import { ConfigKey } from '../../../common/constants/monitor_management';
import { RouteContext } from '../types';
import {
@ -115,7 +116,7 @@ export class OverviewStatusService {
];
};
const filters: QueryDslQueryContainer[] = [
...(showFromAllSpaces ? [] : [{ term: { 'meta.space_id': spaceId } }]),
...(showFromAllSpaces ? [] : [{ terms: { 'meta.space_id': [spaceId, ALL_SPACES_ID] } }]),
...getTermFilter('monitor.type', monitorTypes),
...getTermFilter('tags', tags),
...getTermFilter('monitor.project.id', projects),
@ -370,7 +371,7 @@ export class OverviewStatusService {
projectId: monitor.attributes[ConfigKey.PROJECT_ID],
isStatusAlertEnabled: isStatusEnabled(monitor.attributes[ConfigKey.ALERT_CONFIG]),
updated_at: monitor.updated_at,
spaceId: monitor.namespaces?.[0],
spaces: monitor.namespaces,
urls: monitor.attributes[ConfigKey.URLS],
maintenanceWindows: monitor.attributes[ConfigKey.MAINTENANCE_WINDOWS]?.map((mw) => mw),
};

View file

@ -7,7 +7,10 @@
import { i18n } from '@kbn/i18n';
import { schema } from '@kbn/config-schema';
import { savedObjectsAdapter } from '../../saved_objects';
import {
getSyntheticsDynamicSettings,
setSyntheticsDynamicSettings,
} from '../../saved_objects/synthetics_settings';
import { SyntheticsRestApiRouteFactory } from '../types';
import { DynamicSettings } from '../../../common/runtime_types';
import { DynamicSettingsAttributes } from '../../runtime_types/settings';
@ -20,8 +23,9 @@ export const createGetDynamicSettingsRoute: SyntheticsRestApiRouteFactory<
path: SYNTHETICS_API_URLS.DYNAMIC_SETTINGS,
validate: false,
handler: async ({ savedObjectsClient }) => {
const dynamicSettingsAttributes: DynamicSettingsAttributes =
await savedObjectsAdapter.getSyntheticsDynamicSettings(savedObjectsClient);
const dynamicSettingsAttributes: DynamicSettingsAttributes = await getSyntheticsDynamicSettings(
savedObjectsClient
);
return fromSettingsAttribute(dynamicSettingsAttributes);
},
});
@ -35,9 +39,9 @@ export const createPostDynamicSettingsRoute: SyntheticsRestApiRouteFactory = ()
writeAccess: true,
handler: async ({ savedObjectsClient, request }): Promise<DynamicSettingsAttributes> => {
const newSettings = request.body;
const prevSettings = await savedObjectsAdapter.getSyntheticsDynamicSettings(savedObjectsClient);
const prevSettings = await getSyntheticsDynamicSettings(savedObjectsClient);
const attr = await savedObjectsAdapter.setSyntheticsDynamicSettings(savedObjectsClient, {
const attr = await setSyntheticsDynamicSettings(savedObjectsClient, {
...prevSettings,
...newSettings,
} as DynamicSettingsAttributes);

View file

@ -6,10 +6,9 @@
*/
import { schema } from '@kbn/config-schema';
import { isEmpty } from 'lodash';
import { getSavedObjectKqlFilter } from '../../common';
import { PRIVATE_LOCATION_WRITE_API } from '../../../feature';
import { migrateLegacyPrivateLocations } from './migrate_legacy_private_locations';
import { getMonitorsByLocation } from './get_location_monitors';
import { getPrivateLocationsAndAgentPolicies } from './get_private_locations';
import { SyntheticsRestApiRouteFactory } from '../../types';
import { SYNTHETICS_API_URLS } from '../../../../common/constants';
@ -28,7 +27,14 @@ export const deletePrivateLocationRoute: SyntheticsRestApiRouteFactory<undefined
},
requiredPrivileges: [PRIVATE_LOCATION_WRITE_API],
handler: async (routeContext) => {
const { savedObjectsClient, syntheticsMonitorClient, request, response, server } = routeContext;
const {
savedObjectsClient,
syntheticsMonitorClient,
request,
response,
server,
monitorConfigRepository,
} = routeContext;
const internalSOClient = server.coreStart.savedObjects.createInternalRepository();
await migrateLegacyPrivateLocations(internalSOClient, server.logger);
@ -49,13 +55,17 @@ export const deletePrivateLocationRoute: SyntheticsRestApiRouteFactory<undefined
});
}
const monitors = await getMonitorsByLocation(server, locationId);
const locationFilter = getSavedObjectKqlFilter({ field: 'locations.id', values: locationId });
if (!isEmpty(monitors)) {
const count = monitors.find((monitor) => monitor.id === locationId)?.count;
const data = await monitorConfigRepository.find({
perPage: 0,
filter: locationFilter,
});
if (data.total > 0) {
return response.badRequest({
body: {
message: `Private location with id ${locationId} cannot be deleted because it is used by ${count} monitor(s).`,
message: `Private location with id ${locationId} cannot be deleted because it is used by ${data.total} monitor(s).`,
},
});
}

View file

@ -6,31 +6,36 @@
*/
import { ALL_SPACES_ID } from '@kbn/spaces-plugin/common/constants';
import { getSavedObjectKqlFilter } from '../../common';
import { getPrivateLocationsAndAgentPolicies } from './get_private_locations';
import { SyntheticsRestApiRouteFactory } from '../../types';
import { SYNTHETICS_API_URLS } from '../../../../common/constants';
import { monitorAttributes, syntheticsMonitorType } from '../../../../common/types/saved_objects';
import { SyntheticsServerSetup } from '../../../types';
import {
legacyMonitorAttributes,
syntheticsMonitorAttributes,
syntheticsMonitorSOTypes,
} from '../../../../common/types/saved_objects';
type Payload = Array<{
id: string;
count: number;
}>;
interface ExpectedResponse {
locations: {
buckets: Array<{
interface Bucket {
key: string;
doc_count: number;
}>;
};
}
const aggs = {
locations_legacy: {
terms: {
field: `${legacyMonitorAttributes}.locations.id`,
size: 20000,
},
},
locations: {
terms: {
field: `${monitorAttributes}.locations.id`,
size: 10000,
field: `${syntheticsMonitorAttributes}.locations.id`,
size: 20000,
},
},
};
@ -40,27 +45,44 @@ export const getLocationMonitors: SyntheticsRestApiRouteFactory<Payload> = () =>
path: SYNTHETICS_API_URLS.PRIVATE_LOCATIONS_MONITORS,
validate: {},
handler: async ({ server }) => {
return await getMonitorsByLocation(server);
},
});
export const getMonitorsByLocation = async (server: SyntheticsServerSetup, locationId?: string) => {
handler: async ({ server, savedObjectsClient, syntheticsMonitorClient }) => {
const soClient = server.coreStart.savedObjects.createInternalRepository();
const locationFilter = getSavedObjectKqlFilter({ field: 'locations.id', values: locationId });
const { locations } = await getPrivateLocationsAndAgentPolicies(
savedObjectsClient,
syntheticsMonitorClient
);
const locationMonitors = await soClient.find<unknown, ExpectedResponse>({
type: syntheticsMonitorType,
const locationMonitors = await soClient.find({
type: syntheticsMonitorSOTypes,
perPage: 0,
aggs,
filter: locationFilter,
namespaces: [ALL_SPACES_ID],
});
return (
locationMonitors.aggregations?.locations.buckets.map(({ key: id, doc_count: count }) => ({
const aggsResp = locationMonitors.aggregations as
| {
locations_legacy?: { buckets: Bucket[] };
locations?: { buckets: Bucket[] };
}
| undefined;
// Merge counts from both buckets
const counts: Record<string, number> = {};
aggsResp?.locations_legacy?.buckets.forEach(({ key, doc_count: docCount }) => {
counts[key] = (counts[key] || 0) + docCount;
});
aggsResp?.locations?.buckets.forEach(({ key, doc_count: docCount }) => {
counts[key] = (counts[key] || 0) + docCount;
});
return Object.entries(counts)
.map(([id, count]) => ({
id,
count,
})) ?? []
}))
.filter(({ id }) =>
locations.some((location) => location.id === id || location.label === id)
);
};
},
});

View file

@ -151,6 +151,7 @@ describe('updatePrivateLocationMonitors', () => {
schedule: { number: '10', unit: 'm' },
namespace: FIRST_SPACE_ID,
},
namespaces: [FIRST_SPACE_ID],
},
{
id: SECOND_MONITOR_ID,
@ -166,6 +167,7 @@ describe('updatePrivateLocationMonitors', () => {
schedule: { number: '5', unit: 'm' },
namespace: SECOND_SPACE_ID,
},
namespaces: [SECOND_SPACE_ID],
},
];

View file

@ -101,21 +101,21 @@ export const updatePrivateLocationMonitors = async ({
monitorWithRevision,
};
const namespace = m.attributes.namespace;
const spaceId = m.namespaces?.[0] || 'default'; // Default to 'default' if no namespace is found
return {
...acc,
[namespace]: [...(acc[namespace] || []), monitorToUpdate],
[spaceId]: [...(acc[spaceId] || []), monitorToUpdate],
};
},
{}
);
const promises = Object.keys(updatedMonitorsPerSpace).map((namespace) => [
const promises = Object.keys(updatedMonitorsPerSpace).map((spaceId) => [
syncEditedMonitorBulk({
monitorsToUpdate: updatedMonitorsPerSpace[namespace],
monitorsToUpdate: updatedMonitorsPerSpace[spaceId],
privateLocations: allPrivateLocations,
routeContext,
spaceId: namespace,
spaceId,
}),
]);

View file

@ -1,166 +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 { SyntheticsRestApiRouteFactory } from '../types';
import { monitorAttributes, syntheticsMonitorType } from '../../../common/types/saved_objects';
import {
ConfigKey,
MonitorFiltersResult,
EncryptedSyntheticsMonitorAttributes,
} from '../../../common/runtime_types';
import { SYNTHETICS_API_URLS } from '../../../common/constants';
import { QuerySchema, getMonitorFilters, SEARCH_FIELDS } from '../common';
import { getAllLocations } from '../../synthetics_service/get_all_locations';
type Buckets = Array<{
key: string;
doc_count: number;
}>;
interface AggsResponse {
locationsAggs: {
buckets: Buckets;
};
tagsAggs: {
buckets: Buckets;
};
projectsAggs: {
buckets: Buckets;
};
monitorTypesAggs: {
buckets: Buckets;
};
monitorIdsAggs: {
buckets: Array<{
key: string;
doc_count: number;
name: {
hits: {
hits: Array<{
_source: {
[syntheticsMonitorType]: {
[ConfigKey.NAME]: string;
};
};
}>;
};
};
}>;
};
}
export const getSyntheticsSuggestionsRoute: SyntheticsRestApiRouteFactory<
MonitorFiltersResult
> = () => ({
method: 'GET',
path: SYNTHETICS_API_URLS.SUGGESTIONS,
validate: {
query: QuerySchema,
},
handler: async (route): Promise<any> => {
const { monitorConfigRepository } = route;
const { query } = route.request.query;
const { filtersStr } = await getMonitorFilters(route);
const { allLocations = [] } = await getAllLocations(route);
const data = await monitorConfigRepository.find<EncryptedSyntheticsMonitorAttributes>({
perPage: 0,
filter: filtersStr ? `${filtersStr}` : undefined,
aggs,
search: query ? `${query}*` : undefined,
searchFields: SEARCH_FIELDS,
});
const { monitorTypesAggs, tagsAggs, locationsAggs, projectsAggs, monitorIdsAggs } =
(data?.aggregations as AggsResponse) ?? {};
const allLocationsMap = new Map(allLocations.map((obj) => [obj.id, obj.label]));
return {
monitorIds: monitorIdsAggs?.buckets?.map(({ key, doc_count: count, name }) => ({
label: name?.hits?.hits[0]?._source?.[syntheticsMonitorType]?.[ConfigKey.NAME] || key,
value: key,
count,
})),
tags:
tagsAggs?.buckets?.map(({ key, doc_count: count }) => ({
label: key,
value: key,
count,
})) ?? [],
locations:
locationsAggs?.buckets?.map(({ key, doc_count: count }) => ({
label: allLocationsMap.get(key) || key,
value: key,
count,
})) ?? [],
projects:
projectsAggs?.buckets?.map(({ key, doc_count: count }) => ({
label: key,
value: key,
count,
})) ?? [],
monitorTypes:
monitorTypesAggs?.buckets?.map(({ key, doc_count: count }) => ({
label: key,
value: key,
count,
})) ?? [],
};
},
});
const aggs = {
tagsAggs: {
terms: {
field: `${monitorAttributes}.${ConfigKey.TAGS}`,
size: 10000,
exclude: [''],
},
},
monitorTypeAggs: {
terms: {
field: `${monitorAttributes}.${ConfigKey.MONITOR_TYPE}.keyword`,
size: 10000,
exclude: [''],
},
},
locationsAggs: {
terms: {
field: `${monitorAttributes}.${ConfigKey.LOCATIONS}.id`,
size: 10000,
exclude: [''],
},
},
projectsAggs: {
terms: {
field: `${monitorAttributes}.${ConfigKey.PROJECT_ID}`,
size: 10000,
exclude: [''],
},
},
monitorTypesAggs: {
terms: {
field: `${monitorAttributes}.${ConfigKey.MONITOR_TYPE}.keyword`,
size: 10000,
exclude: [''],
},
},
monitorIdsAggs: {
terms: {
field: `${monitorAttributes}.${ConfigKey.MONITOR_QUERY_ID}`,
size: 10000,
exclude: [''],
},
aggs: {
name: {
top_hits: {
_source: [`${syntheticsMonitorType}.${ConfigKey.NAME}`],
size: 1,
},
},
},
},
};

View file

@ -0,0 +1,261 @@
/*
* 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 {
syntheticsMonitorAttributes,
syntheticsMonitorSavedObjectType,
legacySyntheticsMonitorTypeSingle,
legacyMonitorAttributes,
} from '../../../common/types/saved_objects';
import { SyntheticsRestApiRouteFactory } from '../types';
import {
ConfigKey,
MonitorFiltersResult,
EncryptedSyntheticsMonitorAttributes,
} from '../../../common/runtime_types';
import { SYNTHETICS_API_URLS } from '../../../common/constants';
import { QuerySchema, getMonitorFilters, SEARCH_FIELDS } from '../common';
import { getAllLocations } from '../../synthetics_service/get_all_locations';
type Buckets = Array<{
key: string;
doc_count: number;
}>;
type MonitorTypes =
| typeof syntheticsMonitorSavedObjectType
| typeof legacySyntheticsMonitorTypeSingle;
interface AggsResponse {
locationsAggs: {
buckets: Buckets;
};
tagsAggs: {
buckets: Buckets;
};
projectsAggs: {
buckets: Buckets;
};
monitorTypesAggs: {
buckets: Buckets;
};
monitorIdsAggs: {
buckets: Array<{
key: string;
doc_count: number;
name: {
hits: {
hits: Array<{
_source: {
[key in MonitorTypes]: {
[ConfigKey.NAME]: string;
};
};
}>;
};
};
}>;
};
}
// Helper to sum buckets by key
function sumBuckets(bucketsA: Buckets = [], bucketsB: Buckets = []): Buckets {
const map = new Map<string, number>();
for (const { key, doc_count: docCount } of bucketsA) {
map.set(key, docCount);
}
for (const { key, doc_count: docCount } of bucketsB) {
map.set(key, (map.get(key) || 0) + docCount);
}
return Array.from(map.entries()).map(([key, docCount]) => ({ key, doc_count: docCount }));
}
// Helper to sum monitorIdsAggs buckets
function sumMonitorIdsBuckets(
bucketsA: AggsResponse['monitorIdsAggs']['buckets'] = [],
bucketsB: AggsResponse['monitorIdsAggs']['buckets'] = []
): AggsResponse['monitorIdsAggs']['buckets'] {
const map = new Map<string, { doc_count: number; name?: any }>();
for (const b of bucketsA) {
map.set(b.key, { doc_count: b.doc_count, name: b.name });
}
for (const b of bucketsB) {
if (map.has(b.key)) {
map.get(b.key)!.doc_count += b.doc_count;
} else {
map.set(b.key, { doc_count: b.doc_count, name: b.name });
}
}
return Array.from(map.entries()).map(([key, { doc_count: docCount, name }]) => ({
key,
doc_count: docCount,
name,
}));
}
// Helper to generate aggs for new or legacy monitors
function getAggs(isLegacy: boolean) {
const attributes = isLegacy ? legacyMonitorAttributes : syntheticsMonitorAttributes;
const savedObjectType = isLegacy
? legacySyntheticsMonitorTypeSingle
: syntheticsMonitorSavedObjectType;
return {
tagsAggs: {
terms: {
field: `${attributes}.${ConfigKey.TAGS}`,
size: 10000,
exclude: [''],
},
},
monitorTypeAggs: {
terms: {
field: `${attributes}.${ConfigKey.MONITOR_TYPE}.keyword`,
size: 10000,
exclude: [''],
},
},
locationsAggs: {
terms: {
field: `${attributes}.${ConfigKey.LOCATIONS}.id`,
size: 10000,
exclude: [''],
},
},
projectsAggs: {
terms: {
field: `${attributes}.${ConfigKey.PROJECT_ID}`,
size: 10000,
exclude: [''],
},
},
monitorTypesAggs: {
terms: {
field: `${attributes}.${ConfigKey.MONITOR_TYPE}.keyword`,
size: 10000,
exclude: [''],
},
},
monitorIdsAggs: {
terms: {
field: `${attributes}.${ConfigKey.MONITOR_QUERY_ID}`,
size: 10000,
exclude: [''],
},
aggs: {
name: {
top_hits: {
_source: [`${savedObjectType}.${ConfigKey.NAME}`],
size: 1,
},
},
},
},
};
}
export const getSyntheticsSuggestionsRoute: SyntheticsRestApiRouteFactory<
MonitorFiltersResult
> = () => ({
method: 'GET',
path: SYNTHETICS_API_URLS.SUGGESTIONS,
validate: {
query: QuerySchema,
},
handler: async (route): Promise<any> => {
const { savedObjectsClient } = route;
const { query } = route.request.query;
const { filtersStr } = await getMonitorFilters(route, syntheticsMonitorAttributes);
const { allLocations = [] } = await getAllLocations(route);
// Find for new monitors
const data = await savedObjectsClient.find<EncryptedSyntheticsMonitorAttributes>({
type: syntheticsMonitorSavedObjectType,
perPage: 0,
filter: filtersStr ? filtersStr : undefined,
aggs: getAggs(false),
search: query ? `${query}*` : undefined,
searchFields: SEARCH_FIELDS,
});
const { filtersStr: legacyFilterStr } = await getMonitorFilters(route, legacyMonitorAttributes);
// Find for legacy monitors
const legacyData = await savedObjectsClient.find<any>({
type: legacySyntheticsMonitorTypeSingle,
perPage: 0,
filter: legacyFilterStr ? legacyFilterStr : undefined,
aggs: getAggs(true),
search: query ? `${query}*` : undefined,
searchFields: SEARCH_FIELDS,
});
// Extract aggs
const { monitorTypesAggs, tagsAggs, locationsAggs, projectsAggs, monitorIdsAggs } =
(data?.aggregations as AggsResponse) ?? {};
const {
monitorTypesAggs: legacyMonitorTypesAggs,
tagsAggs: legacyTagsAggs,
locationsAggs: legacyLocationsAggs,
projectsAggs: legacyProjectsAggs,
monitorIdsAggs: legacyMonitorIdsAggs,
} = (legacyData?.aggregations as AggsResponse) ?? {};
const allLocationsMap = new Map(allLocations.map((obj) => [obj.id, obj.label]));
// Sum buckets
const summedTags = sumBuckets(tagsAggs?.buckets, legacyTagsAggs?.buckets);
const summedLocations = sumBuckets(locationsAggs?.buckets, legacyLocationsAggs?.buckets);
const summedProjects = sumBuckets(projectsAggs?.buckets, legacyProjectsAggs?.buckets);
const summedMonitorTypes = sumBuckets(
monitorTypesAggs?.buckets,
legacyMonitorTypesAggs?.buckets
);
const summedMonitorIds = sumMonitorIdsBuckets(
monitorIdsAggs?.buckets,
legacyMonitorIdsAggs?.buckets
);
return {
monitorIds: summedMonitorIds?.map(({ key, doc_count: count, name }) => {
const source = name?.hits?.hits[0]?._source || {};
return {
label:
source?.[syntheticsMonitorSavedObjectType]?.[ConfigKey.NAME] ||
source?.[legacySyntheticsMonitorTypeSingle]?.[ConfigKey.NAME] ||
key,
value: key,
count,
};
}),
tags:
summedTags?.map(({ key, doc_count: count }) => ({
label: key,
value: key,
count,
})) ?? [],
locations:
summedLocations?.map(({ key, doc_count: count }) => ({
label: allLocationsMap.get(key) || key,
value: key,
count,
})) ?? [],
projects:
summedProjects?.map(({ key, doc_count: count }) => ({
label: key,
value: key,
count,
})) ?? [],
monitorTypes:
summedMonitorTypes?.map(({ key, doc_count: count }) => ({
label: key,
value: key,
count,
})) ?? [],
};
},
});

View file

@ -5,7 +5,6 @@
* 2.0.
*/
import { schema } from '@kbn/config-schema';
import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common';
import { isEmpty } from 'lodash';
import { PrivateLocationAttributes } from '../../runtime_types/private_locations';
import { getPrivateLocationsForMonitor } from '../monitor_cruds/add_monitor/utils';
@ -26,9 +25,9 @@ export const runOnceSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () =
handler: async ({
request,
response,
server,
syntheticsMonitorClient,
savedObjectsClient,
spaceId,
}): Promise<any> => {
const monitor = request.body as MonitorFields;
const { monitorId } = request.params;
@ -36,9 +35,7 @@ export const runOnceSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () =
return response.badRequest({ body: { message: 'Monitor data is empty.' } });
}
const validationResult = validateMonitor(monitor);
const spaceId = server.spaces?.spacesService.getSpaceId(request) ?? DEFAULT_SPACE_ID;
const validationResult = validateMonitor(monitor, spaceId);
const decodedMonitor = validationResult.decodedMonitor;
if (!validationResult.valid || !decodedMonitor) {

View file

@ -8,13 +8,11 @@ import { schema } from '@kbn/config-schema';
import { v4 as uuidv4 } from 'uuid';
import { SavedObjectsErrorHelpers } from '@kbn/core-saved-objects-server';
import { IKibanaResponse } from '@kbn/core-http-server';
import { getDecryptedMonitor } from '../../saved_objects/synthetics_monitor';
import { PrivateLocationAttributes } from '../../runtime_types/private_locations';
import { RouteContext, SyntheticsRestApiRouteFactory } from '../types';
import { TestNowResponse } from '../../../common/types';
import { ConfigKey, MonitorFields } from '../../../common/runtime_types';
import { SYNTHETICS_API_URLS } from '../../../common/constants';
import { normalizeSecrets } from '../../synthetics_service/utils/secrets';
import { getPrivateLocationsForMonitor } from '../monitor_cruds/add_monitor/utils';
import { getMonitorNotFoundResponse } from './service_errors';
@ -37,14 +35,19 @@ export const triggerTestNow = async (
monitorId: string,
routeContext: RouteContext
): Promise<TestNowResponse | IKibanaResponse<any>> => {
const { server, spaceId, syntheticsMonitorClient, savedObjectsClient, response } = routeContext;
const {
spaceId,
syntheticsMonitorClient,
savedObjectsClient,
response,
monitorConfigRepository,
} = routeContext;
try {
const monitorWithSecrets = await getDecryptedMonitor(server, monitorId, spaceId);
const normalizedMonitor = normalizeSecrets(monitorWithSecrets);
const { normalizedMonitor } = await monitorConfigRepository.getDecrypted(monitorId, spaceId);
const { [ConfigKey.SCHEDULE]: schedule, [ConfigKey.LOCATIONS]: locations } =
monitorWithSecrets.attributes;
normalizedMonitor.attributes;
const privateLocations: PrivateLocationAttributes[] = await getPrivateLocationsForMonitor(
savedObjectsClient,

View file

@ -5,4 +5,7 @@
* 2.0.
*/
export { savedObjectsAdapter } from './saved_objects';
export {
LEGACY_SYNTHETICS_MONITOR_ENCRYPTED_TYPE,
LEGACY_SYNTHETICS_MONITOR_ENCRYPTED_TYPE_SINGLE,
} from './synthetics_monitor/legacy_synthetics_monitor';

View file

@ -10,7 +10,7 @@ import {
ConfigKey,
SyntheticsMonitorWithSecretsAttributes,
} from '../../../../common/runtime_types';
import { LEGACY_SYNTHETICS_MONITOR_ENCRYPTED_TYPE } from '../../synthetics_monitor';
import { LEGACY_SYNTHETICS_MONITOR_ENCRYPTED_TYPE } from '../../synthetics_monitor/legacy_synthetics_monitor';
export type SyntheticsMonitorWithSecretsAttributes860 = Omit<
SyntheticsMonitorWithSecretsAttributes,

View file

@ -165,6 +165,7 @@ describe('Monitor migrations v8.7.0 -> v8.8.0', () => {
urls: 'https://elastic.co',
labels: {},
maintenance_windows: [],
spaces: [],
},
coreMigrationVersion: '8.8.0',
created_at: '2023-03-31T20:31:24.177Z',

View file

@ -25,8 +25,8 @@ import {
} from '../../../../common/constants/monitor_defaults';
import {
LEGACY_SYNTHETICS_MONITOR_ENCRYPTED_TYPE,
SYNTHETICS_MONITOR_ENCRYPTED_TYPE,
} from '../../synthetics_monitor';
LEGACY_SYNTHETICS_MONITOR_ENCRYPTED_TYPE_SINGLE,
} from '../../synthetics_monitor/legacy_synthetics_monitor';
import { validateMonitor } from '../../../routes/monitor_cruds/monitor_validation';
import {
formatSecrets,
@ -89,7 +89,7 @@ export const migration880 = (encryptedSavedObjects: EncryptedSavedObjectsPluginS
return migrated;
},
inputType: LEGACY_SYNTHETICS_MONITOR_ENCRYPTED_TYPE,
migratedType: SYNTHETICS_MONITOR_ENCRYPTED_TYPE,
migratedType: LEGACY_SYNTHETICS_MONITOR_ENCRYPTED_TYPE_SINGLE,
});
};
@ -117,10 +117,13 @@ const omitZipUrlFields = (fields: BrowserFields) => {
// will return only fields that match the current type defs, which omit
// zip url fields
const validationResult = validateMonitor({
const validationResult = validateMonitor(
{
...fields,
[ConfigKey.METADATA]: updatedMetadata,
} as MonitorFields);
} as MonitorFields,
fields[ConfigKey.ORIGINAL_SPACE]!
);
if (!validationResult.valid || !validationResult.decodedMonitor) {
throw new Error(

View file

@ -6,11 +6,11 @@
*/
import { EncryptedSavedObjectsPluginSetup } from '@kbn/encrypted-saved-objects-plugin/server';
import { SavedObjectUnsanitizedDoc } from '@kbn/core/server';
import { LEGACY_SYNTHETICS_MONITOR_ENCRYPTED_TYPE_SINGLE } from '../../synthetics_monitor/legacy_synthetics_monitor';
import {
ConfigKey,
SyntheticsMonitorWithSecretsAttributes,
} from '../../../../common/runtime_types';
import { SYNTHETICS_MONITOR_ENCRYPTED_TYPE } from '../../synthetics_monitor';
export type SyntheticsMonitor890 = Omit<
SyntheticsMonitorWithSecretsAttributes,
@ -51,7 +51,7 @@ export const migration890 = (encryptedSavedObjects: EncryptedSavedObjectsPluginS
return migrated;
},
inputType: SYNTHETICS_MONITOR_ENCRYPTED_TYPE,
migratedType: SYNTHETICS_MONITOR_ENCRYPTED_TYPE,
inputType: LEGACY_SYNTHETICS_MONITOR_ENCRYPTED_TYPE_SINGLE,
migratedType: LEGACY_SYNTHETICS_MONITOR_ENCRYPTED_TYPE_SINGLE,
});
};

View file

@ -5,35 +5,26 @@
* 2.0.
*/
import {
SavedObjectsClientContract,
SavedObjectsErrorHelpers,
SavedObjectsServiceSetup,
} from '@kbn/core/server';
import { SavedObjectsServiceSetup } from '@kbn/core/server';
import { EncryptedSavedObjectsPluginSetup } from '@kbn/encrypted-saved-objects-plugin/server';
import { fromSettingsAttribute } from '../routes/settings/dynamic_settings';
import {
syntheticsSettings,
syntheticsSettingsObjectId,
syntheticsSettingsObjectType,
uptimeSettingsObjectId,
uptimeSettingsObjectType,
} from './synthetics_settings';
getSyntheticsMonitorConfigSavedObjectType,
SYNTHETICS_MONITOR_ENCRYPTED_TYPE,
} from './synthetics_monitor/synthetics_monitor_config';
import { syntheticsSettings } from './synthetics_settings';
import {
SYNTHETICS_SECRET_ENCRYPTED_TYPE,
SYNTHETICS_PARAMS_SECRET_ENCRYPTED_TYPE,
syntheticsParamSavedObjectType,
} from './synthetics_param';
import {
LEGACY_PRIVATE_LOCATIONS_SAVED_OBJECT_TYPE,
PRIVATE_LOCATION_SAVED_OBJECT_TYPE,
} from './private_locations';
import { DYNAMIC_SETTINGS_DEFAULT_ATTRIBUTES } from '../constants/settings';
import { DynamicSettingsAttributes } from '../runtime_types/settings';
import {
getSyntheticsMonitorSavedObjectType,
SYNTHETICS_MONITOR_ENCRYPTED_TYPE,
} from './synthetics_monitor';
getLegacySyntheticsMonitorSavedObjectType,
LEGACY_SYNTHETICS_MONITOR_ENCRYPTED_TYPE_SINGLE,
} from './synthetics_monitor/legacy_synthetics_monitor';
import { syntheticsServiceApiKey } from './service_api_key';
export const registerSyntheticsSavedObjects = (
@ -43,64 +34,27 @@ export const registerSyntheticsSavedObjects = (
savedObjectsService.registerType(LEGACY_PRIVATE_LOCATIONS_SAVED_OBJECT_TYPE);
savedObjectsService.registerType(PRIVATE_LOCATION_SAVED_OBJECT_TYPE);
savedObjectsService.registerType(getSyntheticsMonitorSavedObjectType(encryptedSavedObjects));
savedObjectsService.registerType(syntheticsServiceApiKey);
savedObjectsService.registerType(syntheticsParamSavedObjectType);
savedObjectsService.registerType(syntheticsSettings);
// legacy synthetics monitor saved object type which is single namespace
savedObjectsService.registerType(
getLegacySyntheticsMonitorSavedObjectType(encryptedSavedObjects)
);
encryptedSavedObjects.registerType(LEGACY_SYNTHETICS_MONITOR_ENCRYPTED_TYPE_SINGLE);
// synthetics monitor config saved object type which supports multiple namespace
savedObjectsService.registerType(getSyntheticsMonitorConfigSavedObjectType());
encryptedSavedObjects.registerType(SYNTHETICS_MONITOR_ENCRYPTED_TYPE);
// service api key saved object type
savedObjectsService.registerType(syntheticsServiceApiKey);
encryptedSavedObjects.registerType({
type: syntheticsServiceApiKey.name,
attributesToEncrypt: new Set(['apiKey']),
attributesToIncludeInAAD: new Set(['id', 'name']),
});
encryptedSavedObjects.registerType(SYNTHETICS_MONITOR_ENCRYPTED_TYPE);
encryptedSavedObjects.registerType(SYNTHETICS_SECRET_ENCRYPTED_TYPE);
};
export const savedObjectsAdapter = {
getSyntheticsDynamicSettings: async (
client: SavedObjectsClientContract
): Promise<DynamicSettingsAttributes> => {
try {
const obj = await client.get<DynamicSettingsAttributes>(
syntheticsSettingsObjectType,
syntheticsSettingsObjectId
);
return fromSettingsAttribute(obj?.attributes ?? DYNAMIC_SETTINGS_DEFAULT_ATTRIBUTES);
} catch (getErr) {
if (SavedObjectsErrorHelpers.isNotFoundError(getErr)) {
// If the object doesn't exist, check to see if uptime settings exist
return getUptimeDynamicSettings(client);
}
throw getErr;
}
},
setSyntheticsDynamicSettings: async (
client: SavedObjectsClientContract,
settings: DynamicSettingsAttributes
) => {
const settingsObject = await client.create<DynamicSettingsAttributes>(
syntheticsSettingsObjectType,
settings,
{
id: syntheticsSettingsObjectId,
overwrite: true,
}
);
return settingsObject.attributes;
},
};
const getUptimeDynamicSettings = async (client: SavedObjectsClientContract) => {
try {
const obj = await client.get<DynamicSettingsAttributes>(
uptimeSettingsObjectType,
uptimeSettingsObjectId
);
return obj?.attributes ?? DYNAMIC_SETTINGS_DEFAULT_ATTRIBUTES;
} catch (getErr) {
return DYNAMIC_SETTINGS_DEFAULT_ATTRIBUTES;
}
// global params saved object type
savedObjectsService.registerType(syntheticsParamSavedObjectType);
encryptedSavedObjects.registerType(SYNTHETICS_PARAMS_SECRET_ENCRYPTED_TYPE);
};

View file

@ -4,300 +4,3 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { EncryptedSavedObjectsPluginSetup } from '@kbn/encrypted-saved-objects-plugin/server';
import { SavedObjectsType } from '@kbn/core/server';
import { i18n } from '@kbn/i18n';
import { SyntheticsMonitorWithSecretsAttributes } from '../../common/runtime_types';
import { SyntheticsServerSetup } from '../types';
import { syntheticsMonitorType } from '../../common/types/saved_objects';
import { ConfigKey, LegacyConfigKey, secretKeys } from '../../common/constants/monitor_management';
import { monitorMigrations } from './migrations/monitors';
const attributesToIncludeInAAD = new Set([
ConfigKey.APM_SERVICE_NAME,
ConfigKey.CUSTOM_HEARTBEAT_ID,
ConfigKey.CONFIG_ID,
ConfigKey.CONFIG_HASH,
ConfigKey.ENABLED,
ConfigKey.FORM_MONITOR_TYPE,
ConfigKey.HOSTS,
ConfigKey.IGNORE_HTTPS_ERRORS,
ConfigKey.MONITOR_SOURCE_TYPE,
ConfigKey.JOURNEY_FILTERS_MATCH,
ConfigKey.JOURNEY_FILTERS_TAGS,
ConfigKey.JOURNEY_ID,
ConfigKey.MAX_REDIRECTS,
ConfigKey.MODE,
ConfigKey.MONITOR_TYPE,
ConfigKey.NAME,
ConfigKey.NAMESPACE,
ConfigKey.LOCATIONS,
ConfigKey.PLAYWRIGHT_OPTIONS,
ConfigKey.ORIGINAL_SPACE,
ConfigKey.PORT,
ConfigKey.PROXY_URL,
ConfigKey.PROXY_USE_LOCAL_RESOLVER,
ConfigKey.RESPONSE_BODY_INDEX,
ConfigKey.RESPONSE_HEADERS_INDEX,
ConfigKey.RESPONSE_BODY_MAX_BYTES,
ConfigKey.RESPONSE_STATUS_CHECK,
ConfigKey.REQUEST_METHOD_CHECK,
ConfigKey.REVISION,
ConfigKey.SCHEDULE,
ConfigKey.SCREENSHOTS,
ConfigKey.IPV4,
ConfigKey.IPV6,
ConfigKey.PROJECT_ID,
ConfigKey.TEXT_ASSERTION,
ConfigKey.TLS_CERTIFICATE_AUTHORITIES,
ConfigKey.TLS_CERTIFICATE,
ConfigKey.TLS_VERIFICATION_MODE,
ConfigKey.TLS_VERSION,
ConfigKey.TAGS,
ConfigKey.TIMEOUT,
ConfigKey.THROTTLING_CONFIG,
ConfigKey.URLS,
ConfigKey.WAIT,
ConfigKey.MONITOR_QUERY_ID,
]);
export const LEGACY_SYNTHETICS_MONITOR_ENCRYPTED_TYPE = {
type: syntheticsMonitorType,
attributesToEncrypt: new Set([
'secrets',
/* adding secretKeys to the list of attributes to encrypt ensures
* that secrets are never stored on the resulting saved object,
* even in the presence of developer error.
*
* In practice, all secrets should be stored as a single JSON
* payload on the `secrets` key. This ensures performant decryption. */
...secretKeys,
]),
attributesToIncludeInAAD: new Set([
LegacyConfigKey.SOURCE_ZIP_URL,
LegacyConfigKey.SOURCE_ZIP_USERNAME,
LegacyConfigKey.SOURCE_ZIP_PASSWORD,
LegacyConfigKey.SOURCE_ZIP_FOLDER,
LegacyConfigKey.SOURCE_ZIP_PROXY_URL,
LegacyConfigKey.ZIP_URL_TLS_CERTIFICATE_AUTHORITIES,
LegacyConfigKey.ZIP_URL_TLS_CERTIFICATE,
LegacyConfigKey.ZIP_URL_TLS_KEY,
LegacyConfigKey.ZIP_URL_TLS_KEY_PASSPHRASE,
LegacyConfigKey.ZIP_URL_TLS_VERIFICATION_MODE,
LegacyConfigKey.ZIP_URL_TLS_VERSION,
LegacyConfigKey.THROTTLING_CONFIG,
LegacyConfigKey.IS_THROTTLING_ENABLED,
LegacyConfigKey.DOWNLOAD_SPEED,
LegacyConfigKey.UPLOAD_SPEED,
LegacyConfigKey.LATENCY,
...attributesToIncludeInAAD,
]),
};
export const SYNTHETICS_MONITOR_ENCRYPTED_TYPE = {
type: syntheticsMonitorType,
attributesToEncrypt: new Set([
'secrets',
/* adding secretKeys to the list of attributes to encrypt ensures
* that secrets are never stored on the resulting saved object,
* even in the presence of developer error.
*
* In practice, all secrets should be stored as a single JSON
* payload on the `secrets` key. This ensures performant decryption. */
...secretKeys,
]),
attributesToIncludeInAAD,
};
export const getSyntheticsMonitorSavedObjectType = (
encryptedSavedObjects: EncryptedSavedObjectsPluginSetup
): SavedObjectsType => {
return {
name: syntheticsMonitorType,
hidden: false,
namespaceType: 'single',
migrations: {
'8.6.0': monitorMigrations['8.6.0'](encryptedSavedObjects),
'8.8.0': monitorMigrations['8.8.0'](encryptedSavedObjects),
'8.9.0': monitorMigrations['8.9.0'](encryptedSavedObjects),
},
mappings: {
dynamic: false,
properties: {
name: {
type: 'text',
fields: {
keyword: {
type: 'keyword',
ignore_above: 256,
normalizer: 'lowercase',
},
},
},
type: {
type: 'text',
fields: {
keyword: {
type: 'keyword',
ignore_above: 256,
},
},
},
urls: {
type: 'text',
fields: {
keyword: {
type: 'keyword',
ignore_above: 256,
},
},
},
hosts: {
type: 'text',
fields: {
keyword: {
type: 'keyword',
ignore_above: 256,
},
},
},
journey_id: {
type: 'keyword',
},
project_id: {
type: 'keyword',
fields: {
text: {
type: 'text',
},
},
},
origin: {
type: 'keyword',
},
hash: {
type: 'keyword',
},
locations: {
properties: {
id: {
type: 'keyword',
ignore_above: 256,
fields: {
text: {
type: 'text',
},
},
},
label: {
type: 'text',
},
},
},
custom_heartbeat_id: {
type: 'keyword',
},
id: {
type: 'keyword',
},
config_id: {
type: 'keyword',
},
tags: {
type: 'keyword',
fields: {
text: {
type: 'text',
},
},
},
schedule: {
properties: {
number: {
type: 'integer',
},
},
},
enabled: {
type: 'boolean',
},
alert: {
properties: {
status: {
properties: {
enabled: {
type: 'boolean',
},
},
},
tls: {
properties: {
enabled: {
type: 'boolean',
},
},
},
},
},
throttling: {
properties: {
label: {
type: 'keyword',
},
},
},
maintenance_windows: {
type: 'keyword',
},
},
},
management: {
importableAndExportable: false,
icon: 'uptimeApp',
getTitle: (savedObject) =>
savedObject.attributes.name +
' - ' +
i18n.translate('xpack.synthetics.syntheticsMonitors.label', {
defaultMessage: 'Synthetics - Monitor',
}),
},
modelVersions: {
'1': {
changes: [
{
type: 'mappings_addition',
addedMappings: {
config_id: { type: 'keyword' },
},
},
],
},
'2': {
changes: [
{
type: 'mappings_addition',
addedMappings: {
maintenance_windows: { type: 'keyword' },
},
},
],
},
},
};
};
export const getDecryptedMonitor = async (
server: SyntheticsServerSetup,
monitorId: string,
spaceId: string
) => {
const encryptedClient = server.encryptedSavedObjects.getClient();
return await encryptedClient.getDecryptedAsInternalUser<SyntheticsMonitorWithSecretsAttributes>(
syntheticsMonitorType,
monitorId,
{
namespace: spaceId,
}
);
};

View file

@ -0,0 +1,13 @@
/*
* 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.
*/
export {
LEGACY_SYNTHETICS_MONITOR_ENCRYPTED_TYPE,
LEGACY_SYNTHETICS_MONITOR_ENCRYPTED_TYPE_SINGLE,
} from './legacy_synthetics_monitor';
export * from './synthetics_monitor_config';

View file

@ -0,0 +1,109 @@
/*
* 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 { EncryptedSavedObjectsPluginSetup } from '@kbn/encrypted-saved-objects-plugin/server';
import { SavedObjectsType } from '@kbn/core/server';
import { i18n } from '@kbn/i18n';
import { attributesToIncludeInAAD } from './synthetics_monitor_config';
import { monitorConfigMappings } from './monitor_mappings';
import { legacySyntheticsMonitorTypeSingle } from '../../../common/types/saved_objects';
import { LegacyConfigKey, secretKeys } from '../../../common/constants/monitor_management';
import { monitorMigrations } from '../migrations/monitors';
export const LEGACY_SYNTHETICS_MONITOR_ENCRYPTED_TYPE_SINGLE = {
type: legacySyntheticsMonitorTypeSingle,
attributesToEncrypt: new Set([
'secrets',
/* adding secretKeys to the list of attributes to encrypt ensures
* that secrets are never stored on the resulting saved object,
* even in the presence of developer error.
*
* In practice, all secrets should be stored as a single JSON
* payload on the `secrets` key. This ensures performant decryption. */
...secretKeys,
]),
attributesToIncludeInAAD,
};
export const getLegacySyntheticsMonitorSavedObjectType = (
encryptedSavedObjects: EncryptedSavedObjectsPluginSetup
): SavedObjectsType => {
return {
name: legacySyntheticsMonitorTypeSingle,
hidden: false,
namespaceType: 'single',
migrations: {
'8.6.0': monitorMigrations['8.6.0'](encryptedSavedObjects),
'8.8.0': monitorMigrations['8.8.0'](encryptedSavedObjects),
'8.9.0': monitorMigrations['8.9.0'](encryptedSavedObjects),
},
mappings: monitorConfigMappings,
management: {
importableAndExportable: false,
icon: 'uptimeApp',
getTitle: (savedObject) =>
i18n.translate('xpack.synthetics.syntheticsMonitors.label.name', {
defaultMessage: '{name} - Synthetics - Monitor',
values: { name: savedObject.attributes.name },
}),
},
modelVersions: {
'1': {
changes: [
{
type: 'mappings_addition',
addedMappings: {
config_id: { type: 'keyword' },
},
},
],
},
'2': {
changes: [
{
type: 'mappings_addition',
addedMappings: {
maintenance_windows: { type: 'keyword' },
},
},
],
},
},
};
};
export const LEGACY_SYNTHETICS_MONITOR_ENCRYPTED_TYPE = {
type: legacySyntheticsMonitorTypeSingle,
attributesToEncrypt: new Set([
'secrets',
/* adding secretKeys to the list of attributes to encrypt ensures
* that secrets are never stored on the resulting saved object,
* even in the presence of developer error.
*
* In practice, all secrets should be stored as a single JSON
* payload on the `secrets` key. This ensures performant decryption. */
...secretKeys,
]),
attributesToIncludeInAAD: new Set([
LegacyConfigKey.SOURCE_ZIP_URL,
LegacyConfigKey.SOURCE_ZIP_USERNAME,
LegacyConfigKey.SOURCE_ZIP_PASSWORD,
LegacyConfigKey.SOURCE_ZIP_FOLDER,
LegacyConfigKey.SOURCE_ZIP_PROXY_URL,
LegacyConfigKey.ZIP_URL_TLS_CERTIFICATE_AUTHORITIES,
LegacyConfigKey.ZIP_URL_TLS_CERTIFICATE,
LegacyConfigKey.ZIP_URL_TLS_KEY,
LegacyConfigKey.ZIP_URL_TLS_KEY_PASSPHRASE,
LegacyConfigKey.ZIP_URL_TLS_VERIFICATION_MODE,
LegacyConfigKey.ZIP_URL_TLS_VERSION,
LegacyConfigKey.THROTTLING_CONFIG,
LegacyConfigKey.IS_THROTTLING_ENABLED,
LegacyConfigKey.DOWNLOAD_SPEED,
LegacyConfigKey.UPLOAD_SPEED,
LegacyConfigKey.LATENCY,
...attributesToIncludeInAAD,
]),
};

View file

@ -0,0 +1,139 @@
/*
* 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 type { SavedObjectsTypeMappingDefinition } from '@kbn/core-saved-objects-server';
export const monitorConfigMappings: SavedObjectsTypeMappingDefinition = {
dynamic: false,
properties: {
name: {
type: 'text',
fields: {
keyword: {
type: 'keyword',
ignore_above: 256,
normalizer: 'lowercase',
},
},
},
type: {
type: 'text',
fields: {
keyword: {
type: 'keyword',
ignore_above: 256,
},
},
},
urls: {
type: 'text',
fields: {
keyword: {
type: 'keyword',
ignore_above: 256,
},
},
},
hosts: {
type: 'text',
fields: {
keyword: {
type: 'keyword',
ignore_above: 256,
},
},
},
journey_id: {
type: 'keyword',
},
project_id: {
type: 'keyword',
fields: {
text: {
type: 'text',
},
},
},
origin: {
type: 'keyword',
},
hash: {
type: 'keyword',
},
locations: {
properties: {
id: {
type: 'keyword',
ignore_above: 256,
fields: {
text: {
type: 'text',
},
},
},
label: {
type: 'text',
},
},
},
custom_heartbeat_id: {
type: 'keyword',
},
id: {
type: 'keyword',
},
config_id: {
type: 'keyword',
},
tags: {
type: 'keyword',
fields: {
text: {
type: 'text',
},
},
},
schedule: {
properties: {
number: {
type: 'integer',
},
},
},
enabled: {
type: 'boolean',
},
alert: {
properties: {
status: {
properties: {
enabled: {
type: 'boolean',
},
},
},
tls: {
properties: {
enabled: {
type: 'boolean',
},
},
},
},
},
throttling: {
properties: {
label: {
type: 'keyword',
},
},
},
maintenance_windows: {
type: 'keyword',
},
},
};

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { processMonitors } from './get_all_monitors';
import { processMonitors } from './process_monitors';
import * as getLocations from '../../synthetics_service/get_all_locations';
describe('processMonitors', () => {

View file

@ -0,0 +1,93 @@
/*
* 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 { SavedObjectsType } from '@kbn/core/server';
import { i18n } from '@kbn/i18n';
import { monitorConfigMappings } from './monitor_mappings';
import { syntheticsMonitorSavedObjectType } from '../../../common/types/saved_objects';
import { ConfigKey, secretKeys } from '../../../common/constants/monitor_management';
export const getSyntheticsMonitorConfigSavedObjectType = (): SavedObjectsType => {
return {
name: syntheticsMonitorSavedObjectType,
hidden: false,
namespaceType: 'multiple',
mappings: monitorConfigMappings,
management: {
importableAndExportable: false,
icon: 'uptimeApp',
getTitle: (savedObject) =>
i18n.translate('xpack.synthetics.syntheticsMonitors.multiple.label', {
defaultMessage: '{name} - (Synthetics Monitor)',
values: { name: savedObject.attributes.name },
}),
},
modelVersions: {},
};
};
export const attributesToIncludeInAAD = new Set([
ConfigKey.APM_SERVICE_NAME,
ConfigKey.CUSTOM_HEARTBEAT_ID,
ConfigKey.CONFIG_ID,
ConfigKey.CONFIG_HASH,
ConfigKey.ENABLED,
ConfigKey.FORM_MONITOR_TYPE,
ConfigKey.HOSTS,
ConfigKey.IGNORE_HTTPS_ERRORS,
ConfigKey.MONITOR_SOURCE_TYPE,
ConfigKey.JOURNEY_FILTERS_MATCH,
ConfigKey.JOURNEY_FILTERS_TAGS,
ConfigKey.JOURNEY_ID,
ConfigKey.MAX_REDIRECTS,
ConfigKey.MODE,
ConfigKey.MONITOR_TYPE,
ConfigKey.NAME,
ConfigKey.NAMESPACE,
ConfigKey.LOCATIONS,
ConfigKey.PLAYWRIGHT_OPTIONS,
ConfigKey.ORIGINAL_SPACE,
ConfigKey.PORT,
ConfigKey.PROXY_URL,
ConfigKey.PROXY_USE_LOCAL_RESOLVER,
ConfigKey.RESPONSE_BODY_INDEX,
ConfigKey.RESPONSE_HEADERS_INDEX,
ConfigKey.RESPONSE_BODY_MAX_BYTES,
ConfigKey.RESPONSE_STATUS_CHECK,
ConfigKey.REQUEST_METHOD_CHECK,
ConfigKey.REVISION,
ConfigKey.SCHEDULE,
ConfigKey.SCREENSHOTS,
ConfigKey.IPV4,
ConfigKey.IPV6,
ConfigKey.PROJECT_ID,
ConfigKey.TEXT_ASSERTION,
ConfigKey.TLS_CERTIFICATE_AUTHORITIES,
ConfigKey.TLS_CERTIFICATE,
ConfigKey.TLS_VERIFICATION_MODE,
ConfigKey.TLS_VERSION,
ConfigKey.TAGS,
ConfigKey.TIMEOUT,
ConfigKey.THROTTLING_CONFIG,
ConfigKey.URLS,
ConfigKey.WAIT,
ConfigKey.MONITOR_QUERY_ID,
]);
export const SYNTHETICS_MONITOR_ENCRYPTED_TYPE = {
type: syntheticsMonitorSavedObjectType,
attributesToEncrypt: new Set([
'secrets',
/* adding secretKeys to the list of attributes to encrypt ensures
* that secrets are never stored on the resulting saved object,
* even in the presence of developer error.
*
* In practice, all secrets should be stored as a single JSON
* payload on the `secrets` key. This ensures performant decryption. */
...secretKeys,
]),
attributesToIncludeInAAD,
};

Some files were not shown because too many files have changed in this diff Show more