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

# Backport

This will backport the following commits from `main` to `8.19`:
- [[Synthetics] Multi space monitors !!
(#221568)](https://github.com/elastic/kibana/pull/221568)

<!--- Backport version: 10.0.1 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sorenlouv/backport)

<!--BACKPORT
[{"author":{"name":"Shahzad","email":"shahzad31comp@gmail.com"},"sourceCommit":{"committedDate":"2025-06-25T08:47:47Z","message":"[Synthetics]
Multi space monitors !! (#221568)\n\n## Summary\n\nMulti space monitors
!!\n\nFixes https://github.com/elastic/kibana/issues/164294\n\nUser will
be able to choose in which space monitors will be available !!\n\n<img
width=\"1728\"
alt=\"image\"\nsrc=\"https://github.com/user-attachments/assets/f01ac226-ed54-4e96-b6f4-27f0134a9be5\"\n/>\n\n\n###
Technical \nThis is being done by registering another saved object type
and for\nexisting monitors it will continue to work as right now but for
newly\ncreated monitors user will have ability to specify spaces or
choose\nmultiple spaces or all.\n\n### Testing\n\n1. Create few monitors
before this PR in multiple spaces\n2. Create multiple monitors in
multiple spaces after this PR\n3. Make sure filtering, editing and
deleting, creating works as expected\non both set of
monitors\n\n---------\n\nCo-authored-by: kibanamachine
<42973632+kibanamachine@users.noreply.github.com>","sha":"f317cec25b3cdfcc7063ff21a4b23e2f9e5f876e","branchLabelMapping":{"^v9.1.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:feature","ci:project-deploy-observability","Team:obs-ux-management","backport:version","v9.1.0","v8.19.0","author:obs-ux-management"],"title":"[Synthetics]
Multi space monitors
!!","number":221568,"url":"https://github.com/elastic/kibana/pull/221568","mergeCommit":{"message":"[Synthetics]
Multi space monitors !! (#221568)\n\n## Summary\n\nMulti space monitors
!!\n\nFixes https://github.com/elastic/kibana/issues/164294\n\nUser will
be able to choose in which space monitors will be available !!\n\n<img
width=\"1728\"
alt=\"image\"\nsrc=\"https://github.com/user-attachments/assets/f01ac226-ed54-4e96-b6f4-27f0134a9be5\"\n/>\n\n\n###
Technical \nThis is being done by registering another saved object type
and for\nexisting monitors it will continue to work as right now but for
newly\ncreated monitors user will have ability to specify spaces or
choose\nmultiple spaces or all.\n\n### Testing\n\n1. Create few monitors
before this PR in multiple spaces\n2. Create multiple monitors in
multiple spaces after this PR\n3. Make sure filtering, editing and
deleting, creating works as expected\non both set of
monitors\n\n---------\n\nCo-authored-by: kibanamachine
<42973632+kibanamachine@users.noreply.github.com>","sha":"f317cec25b3cdfcc7063ff21a4b23e2f9e5f876e"}},"sourceBranch":"main","suggestedTargetBranches":["8.19"],"targetPullRequestStates":[{"branch":"main","label":"v9.1.0","branchLabelMappingKey":"^v9.1.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/221568","number":221568,"mergeCommit":{"message":"[Synthetics]
Multi space monitors !! (#221568)\n\n## Summary\n\nMulti space monitors
!!\n\nFixes https://github.com/elastic/kibana/issues/164294\n\nUser will
be able to choose in which space monitors will be available !!\n\n<img
width=\"1728\"
alt=\"image\"\nsrc=\"https://github.com/user-attachments/assets/f01ac226-ed54-4e96-b6f4-27f0134a9be5\"\n/>\n\n\n###
Technical \nThis is being done by registering another saved object type
and for\nexisting monitors it will continue to work as right now but for
newly\ncreated monitors user will have ability to specify spaces or
choose\nmultiple spaces or all.\n\n### Testing\n\n1. Create few monitors
before this PR in multiple spaces\n2. Create multiple monitors in
multiple spaces after this PR\n3. Make sure filtering, editing and
deleting, creating works as expected\non both set of
monitors\n\n---------\n\nCo-authored-by: kibanamachine
<42973632+kibanamachine@users.noreply.github.com>","sha":"f317cec25b3cdfcc7063ff21a4b23e2f9e5f876e"}},{"branch":"8.19","label":"v8.19.0","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"}]}]
BACKPORT-->

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Shahzad 2025-06-26 23:09:31 +02:00 committed by GitHub
parent 4879e1433f
commit da3522b8e4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
138 changed files with 3573 additions and 1532 deletions

View file

@ -1121,6 +1121,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

@ -3709,6 +3709,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

@ -168,6 +168,7 @@ describe('checking migration metadata changes on all registered SO types', () =>
"spaces-usage-stats": "3abca98713c52af8b30300e386c7779b3025a20e",
"synthetics-dynamic-settings": "4b40a93eb3e222619bf4e7fe34a9b9e7ab91a0a7",
"synthetics-monitor": "078401644f1a6cecdd4294093df20f8ff4063405",
"synthetics-monitor-multi-space": "bb0beffe66446d1cf5306b675a8831794e6d6e36",
"synthetics-param": "3ebb744e5571de678b1312d5c418c8188002cf5e",
"synthetics-private-location": "8cecc9e4f39637d2f8244eb7985c0690ceab24be",
"synthetics-privates-locations": "f53d799d5c9bc8454aaa32c6abc99a899b025d5c",

View file

@ -143,6 +143,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

@ -48222,7 +48222,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

@ -48185,7 +48185,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

@ -48264,7 +48264,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

@ -207,7 +207,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,17 +50,19 @@ export const MonitorForm: FC<
} = methods;
return (
<FormProvider {...methods}>
<EuiForm
isInvalid={Boolean(isSubmitted && Object.keys(errors).length)}
component="form"
noValidate
>
{children}
<EuiSpacer />
<ActionBar readOnly={readOnly} canUsePublicLocations={canUsePublicLocations} />
</EuiForm>
<Disclaimer />
</FormProvider>
<ContextWrapper>
<FormProvider {...methods}>
<EuiForm
isInvalid={Boolean(isSubmitted && Object.keys(errors).length)}
component="form"
noValidate
>
{children}
<EuiSpacer />
<ActionBar readOnly={readOnly} canUsePublicLocations={canUsePublicLocations} />
</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 = (
theme: ReturnType<typeof useTheme>,
@ -163,7 +163,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;
@ -166,52 +166,56 @@ export const OverviewGrid = memo(
minimumBatchSize={MIN_BATCH_SIZE}
threshold={LIST_THRESHOLD}
>
{({ onItemsRendered }) => (
<FixedSizeList
// pad computed height to avoid clipping last row's drop shadow
height={listHeight + 16}
width={width}
onItemsRendered={onItemsRendered}
itemSize={ITEM_HEIGHT}
itemCount={listItems.length}
itemData={listItems}
ref={listRef}
>
{({
index: listIndex,
style,
data: listData,
}: React.PropsWithChildren<ListChildComponentProps<ListItem[][]>>) => {
setCurrentIndex(listIndex);
return (
<EuiFlexGroup
data-test-subj={`overview-grid-row-${listIndex}`}
gutterSize="m"
style={{ ...style }}
>
{listData[listIndex].map((_, idx) => (
<EuiFlexItem
data-test-subj="syntheticsOverviewGridItem"
key={listIndex * ROW_COUNT + idx}
>
<MetricItem
monitor={
monitorsSortedByStatus[listIndex * ROW_COUNT + idx]
}
onClick={setFlyoutConfigCallback}
/>
</EuiFlexItem>
))}
{listData[listIndex].length % ROW_COUNT !== 0 &&
// Adds empty items to fill out row
Array.from({
length: ROW_COUNT - listData[listIndex].length,
}).map((_, idx) => <EuiFlexItem key={idx} />)}
</EuiFlexGroup>
);
}}
</FixedSizeList>
)}
{({ onItemsRendered, ref }) => {
return (
<FixedSizeList
// pad computed height to avoid clipping last row's drop shadow
height={listHeight + 16}
width={width}
onItemsRendered={onItemsRendered}
itemSize={ITEM_HEIGHT}
itemCount={listItems.length}
itemData={listItems}
ref={ref}
>
{({
index: listIndex,
style,
data: listData,
}: React.PropsWithChildren<
ListChildComponentProps<ListItem[][]>
>) => {
setCurrentIndex(listIndex);
return (
<EuiFlexGroup
data-test-subj={`overview-grid-row-${listIndex}`}
gutterSize="m"
css={{ ...style, marginLeft: 5 }}
>
{listData[listIndex].map((_, idx) => (
<EuiFlexItem
data-test-subj="syntheticsOverviewGridItem"
key={listIndex * ROW_COUNT + idx}
>
<MetricItem
monitor={
monitorsSortedByStatus[listIndex * ROW_COUNT + idx]
}
onClick={setFlyoutConfigCallback}
/>
</EuiFlexItem>
))}
{listData[listIndex].length % ROW_COUNT !== 0 &&
// Adds empty items to fill out row
Array.from({
length: ROW_COUNT - listData[listIndex].length,
}).map((_, idx) => <EuiFlexItem key={idx} />)}
</EuiFlexGroup>
);
}}
</FixedSizeList>
);
}}
</InfiniteLoader>
)}
</EuiAutoSizer>

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,
@ -49,8 +50,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 = {}
@ -105,12 +105,12 @@ describe('tlsRuleExecutor', () => {
const monitorId = randomUUID();
const tlsRule = new TLSRuleExecutor(...getTLSRuleExecutorParams({ monitorIds: [monitorId] }));
const configRepo = tlsRule.monitorConfigRepository;
const spy = jest.spyOn(configRepo, 'getAll').mockResolvedValue([]);
const getAllMock = jest.spyOn(configRepo, 'getAll').mockResolvedValue([]);
await tlsRule.getMonitors();
expect(spy).toHaveBeenCalledWith({
filter: `${commonFilter} AND synthetics-monitor.attributes.id:(\"${monitorId}\")`,
expect(getAllMock).toHaveBeenCalledWith({
filter: `${commonFilter} AND synthetics-monitor-multi-space.attributes.id:(\"${monitorId}\")`,
});
});
@ -118,12 +118,12 @@ describe('tlsRuleExecutor', () => {
const tag = 'myMonitor';
const tlsRule = new TLSRuleExecutor(...getTLSRuleExecutorParams({ tags: [tag] }));
const configRepo = tlsRule.monitorConfigRepository;
const spy = jest.spyOn(configRepo, 'getAll').mockResolvedValue([]);
const getAllMock = jest.spyOn(configRepo, 'getAll').mockResolvedValue([]);
await tlsRule.getMonitors();
expect(spy).toHaveBeenCalledWith({
filter: `${commonFilter} AND synthetics-monitor.attributes.tags:(\"${tag}\")`,
expect(getAllMock).toHaveBeenCalledWith({
filter: `${commonFilter} AND synthetics-monitor-multi-space.attributes.tags:(\"${tag}\")`,
});
});
@ -133,12 +133,12 @@ describe('tlsRuleExecutor', () => {
...getTLSRuleExecutorParams({ monitorTypes: [monitorType] })
);
const configRepo = tlsRule.monitorConfigRepository;
const spy = jest.spyOn(configRepo, 'getAll').mockResolvedValue([]);
const getAllMock = jest.spyOn(configRepo, 'getAll').mockResolvedValue([]);
await tlsRule.getMonitors();
expect(spy).toHaveBeenCalledWith({
filter: `${commonFilter} AND synthetics-monitor.attributes.type:(\"${monitorType}\")`,
expect(getAllMock).toHaveBeenCalledWith({
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 { MonitorConfigRepository } from '../../services/monitor_config_repository';
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';
@ -44,10 +44,10 @@ export class TLSRuleExecutor {
server: SyntheticsServerSetup;
syntheticsMonitorClient: SyntheticsMonitorClient;
monitors: Array<SavedObjectsFindResult<EncryptedSyntheticsMonitorAttributes>> = [];
monitorConfigRepository: MonitorConfigRepository;
logger: Logger;
spaceId: string;
ruleName: string;
monitorConfigRepository: MonitorConfigRepository;
constructor(
previousStartedAt: Date | null,
@ -67,13 +67,13 @@ export class TLSRuleExecutor {
});
this.server = server;
this.syntheticsMonitorClient = syntheticsMonitorClient;
this.logger = server.logger;
this.spaceId = spaceId;
this.ruleName = ruleName;
this.monitorConfigRepository = new MonitorConfigRepository(
soClient,
server.encryptedSavedObjects.getClient()
);
this.logger = server.logger;
this.spaceId = spaceId;
this.ruleName = ruleName;
}
debug(message: string) {
@ -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

@ -1,39 +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 { SavedObject } from '@kbn/core/server';
import { EncryptedSavedObjectsClient } from '@kbn/encrypted-saved-objects-plugin/server';
import { syntheticsMonitorType } from '../../common/types/saved_objects';
import {
SyntheticsMonitorWithSecretsAttributes,
SyntheticsMonitor,
} from '../../common/runtime_types';
import { normalizeSecrets } from '../synthetics_service/utils/secrets';
export const getSyntheticsMonitor = async ({
monitorId,
encryptedSavedObjectsClient,
spaceId,
}: {
monitorId: string;
spaceId: string;
encryptedSavedObjectsClient: EncryptedSavedObjectsClient;
}): Promise<SavedObject<SyntheticsMonitor>> => {
try {
const decryptedMonitor =
await encryptedSavedObjectsClient.getDecryptedAsInternalUser<SyntheticsMonitorWithSecretsAttributes>(
syntheticsMonitorType,
monitorId,
{
namespace: spaceId,
}
);
return normalizeSecrets(decryptedMonitor);
} catch (e) {
throw e;
}
};

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, syntheticsMonitorType } 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,37 +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.savedObjectsClient.find({
type: syntheticsMonitorType,
perPage,
page,
sortField: parseMappingKey(sortField),
sortOrder,
searchFields: SEARCH_FIELDS,
search: query ? `${query}*` : undefined,
filter: filtersStr,
searchAfter,
fields,
...(showFromAllSpaces && { namespaces: ['*'] }),
});
};
interface Filters {
filter?: string;
tags?: string | string[];
@ -118,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,
@ -142,7 +110,8 @@ export const getMonitorFilters = async (
monitorQueryIds,
locations,
},
useLogicalAndFor
useLogicalAndFor,
attr
);
};
@ -157,7 +126,8 @@ export const parseArrayFilters = (
monitorQueryIds,
locations,
}: Filters,
useLogicalAndFor: MonitorsQuery['useLogicalAndFor'] = []
useLogicalAndFor: MonitorsQuery['useLogicalAndFor'] = [],
attributes: string = syntheticsMonitorAttributes
) => {
const filtersStr = [
filter,
@ -165,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 ');
@ -188,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;
@ -205,7 +179,7 @@ export const getSavedObjectKqlFilter = ({
if (searchAtRoot) {
fieldKey = `${field}`;
} else {
fieldKey = `${monitorAttributes}.${field}`;
fieldKey = `${attributes}.${field}`;
}
if (Array.isArray(values)) {
@ -283,7 +257,7 @@ export const isMonitorsQueryFiltered = (monitorQuery: MonitorsQuery) => {
);
};
function parseMappingKey(key: string | undefined) {
export function parseMappingKey(key: string | undefined) {
switch (key) {
case 'schedule.keyword':
return 'schedule.number';

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 { 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: `${syntheticsMonitorType}.attributes.${ConfigKey.MONITOR_TYPE}.keyword`,
field: `${syntheticsMonitorAttributes}.${ConfigKey.MONITOR_TYPE}.keyword`,
size: 10000,
},
},
tags: {
terms: {
field: `${syntheticsMonitorType}.attributes.${ConfigKey.TAGS}`,
field: `${syntheticsMonitorAttributes}.${ConfigKey.TAGS}`,
size: 10000,
},
},
locations: {
terms: {
field: `${syntheticsMonitorType}.attributes.${ConfigKey.LOCATIONS}.id`,
field: `${syntheticsMonitorAttributes}.${ConfigKey.LOCATIONS}.id`,
size: 10000,
},
},
projects: {
terms: {
field: `${syntheticsMonitorType}.attributes.${ConfigKey.PROJECT_ID}`,
field: `${syntheticsMonitorAttributes}.${ConfigKey.PROJECT_ID}`,
size: 10000,
},
},
schedules: {
terms: {
field: `${syntheticsMonitorType}.attributes.${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

@ -7,14 +7,17 @@
import { v4 as uuidV4 } from 'uuid';
import { SavedObject } from '@kbn/core-saved-objects-common/src/server_types';
import { SavedObjectsClientContract } from '@kbn/core-saved-objects-api-server';
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, syntheticsMonitorType } from '../../../../common/types/saved_objects';
import { PrivateLocationAttributes } from '../../../runtime_types/private_locations';
import { ConfigKey } from '../../../../common/constants/monitor_management';
import {
@ -37,7 +40,6 @@ import { triggerTestNow } from '../../synthetics_service/test_now_monitor';
import { DefaultAlertService } from '../../default_alerts/default_alert_service';
import { RouteContext } from '../../types';
import { formatTelemetryEvent, sendTelemetryEvents } from '../../telemetry/monitor_upgrade_sender';
import { formatSecrets } from '../../../synthetics_service/utils';
import { formatKibanaNamespace } from '../../../../common/formatters';
import { getPrivateLocations } from '../../../synthetics_service/get_private_locations';
@ -59,11 +61,13 @@ export class AddEditMonitorAPI {
async syncNewMonitor({
id,
normalizedMonitor,
savedObjectType,
}: {
id?: string;
normalizedMonitor: SyntheticsMonitor;
savedObjectType?: string;
}) {
const { savedObjectsClient, server, syntheticsMonitorClient, spaceId } = this.routeContext;
const { server, syntheticsMonitorClient, spaceId } = this.routeContext;
const newMonitorId = id ?? uuidV4();
let monitorSavedObject: SavedObject<EncryptedSyntheticsMonitorAttributes> | null = null;
@ -73,10 +77,11 @@ export class AddEditMonitorAPI {
});
try {
const newMonitorPromise = this.createNewSavedObjectMonitor({
const newMonitorPromise = this.routeContext.monitorConfigRepository.create({
normalizedMonitor: monitorWithNamespace,
id: newMonitorId,
savedObjectsClient,
spaceId,
savedObjectType,
});
const syncErrorsPromise = syntheticsMonitorClient.addMonitors(
@ -125,32 +130,6 @@ export class AddEditMonitorAPI {
}
}
async createNewSavedObjectMonitor({
id,
savedObjectsClient,
normalizedMonitor,
}: {
id: string;
savedObjectsClient: SavedObjectsClientContract;
normalizedMonitor: SyntheticsMonitor;
}) {
return await savedObjectsClient.create<EncryptedSyntheticsMonitorAttributes>(
syntheticsMonitorType,
formatSecrets({
...normalizedMonitor,
[ConfigKey.MONITOR_QUERY_ID]: normalizedMonitor[ConfigKey.CUSTOM_HEARTBEAT_ID] || id,
[ConfigKey.CONFIG_ID]: id,
revision: 1,
}),
id
? {
id,
overwrite: true,
}
: undefined
);
}
validateMonitorType(monitorFields: MonitorFields, previousMonitor?: MonitorFields) {
const { [ConfigKey.MONITOR_TYPE]: monitorType } = monitorFields;
if (previousMonitor) {
@ -230,12 +209,13 @@ export class AddEditMonitorAPI {
}
async validateUniqueMonitorName(name: string, id?: string) {
const { savedObjectsClient } = this.routeContext;
const { monitorConfigRepository } = this.routeContext;
const kqlFilter = getSavedObjectKqlFilter({ field: 'name.keyword', values: name });
const { total } = await savedObjectsClient.find({
const { total } = await monitorConfigRepository.find({
perPage: 0,
type: syntheticsMonitorType,
filter: id ? `${kqlFilter} and not (${monitorAttributes}.config_id: ${id})` : kqlFilter,
filter: id
? `${kqlFilter} and not (${syntheticsMonitorAttributes}.config_id: ${id})`
: kqlFilter,
});
if (total > 0) {
@ -247,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);
@ -331,14 +316,14 @@ export class AddEditMonitorAPI {
}
async revertMonitorIfCreated({ newMonitorId }: { newMonitorId: string }) {
const { server, savedObjectsClient } = this.routeContext;
const { server, monitorConfigRepository } = this.routeContext;
try {
const encryptedMonitor = await savedObjectsClient.get<EncryptedSyntheticsMonitorAttributes>(
syntheticsMonitorType,
newMonitorId
);
const encryptedMonitor = await monitorConfigRepository.get(newMonitorId);
if (encryptedMonitor) {
await savedObjectsClient.delete(syntheticsMonitorType, newMonitorId);
await monitorConfigRepository.bulkDelete([
{ id: newMonitorId, type: syntheticsMonitorSavedObjectType },
{ id: newMonitorId, type: legacySyntheticsMonitorTypeSingle },
]);
const deleteMonitorAPI = new DeleteMonitorAPI(this.routeContext);
await deleteMonitorAPI.execute({

View file

@ -4,7 +4,7 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { SavedObjectsClientContract, SavedObject } from '@kbn/core/server';
import { SavedObject } from '@kbn/core/server';
import pMap from 'p-map';
import { SavedObjectsBulkResponse } from '@kbn/core-saved-objects-api-server';
import { v4 as uuidV4 } from 'uuid';
@ -13,8 +13,6 @@ import { SavedObjectError } from '@kbn/core-saved-objects-common';
import { SyntheticsServerSetup } from '../../../types';
import { RouteContext } from '../../types';
import { formatTelemetryEvent, sendTelemetryEvents } from '../../telemetry/monitor_upgrade_sender';
import { formatSecrets } from '../../../synthetics_service/utils';
import { syntheticsMonitorType } from '../../../../common/types/saved_objects';
import {
ConfigKey,
EncryptedSyntheticsMonitorAttributes,
@ -25,28 +23,6 @@ import {
} from '../../../../common/runtime_types';
import { DeleteMonitorAPI } from '../services/delete_monitor_api';
export const createNewSavedObjectMonitorBulk = async ({
soClient,
monitorsToCreate,
}: {
soClient: SavedObjectsClientContract;
monitorsToCreate: Array<{ id: string; monitor: MonitorFields }>;
}) => {
const newMonitors = monitorsToCreate.map(({ id, monitor }) => ({
id,
type: syntheticsMonitorType,
attributes: formatSecrets({
...monitor,
[ConfigKey.MONITOR_QUERY_ID]: monitor[ConfigKey.CUSTOM_HEARTBEAT_ID] || id,
[ConfigKey.CONFIG_ID]: id,
revision: 1,
}),
}));
const result = await soClient.bulkCreate<EncryptedSyntheticsMonitorAttributes>(newMonitors);
return result.saved_objects;
};
type MonitorSavedObject = SavedObject<EncryptedSyntheticsMonitorAttributes>;
type CreatedMonitors =
@ -63,7 +39,8 @@ export const syncNewMonitorBulk = async ({
privateLocations: SyntheticsPrivateLocations;
spaceId: string;
}) => {
const { server, savedObjectsClient, syntheticsMonitorClient } = routeContext;
const { server, syntheticsMonitorClient, monitorConfigRepository, request } = routeContext;
const { query } = request;
let newMonitors: CreatedMonitors | null = null;
const monitorsToCreate = normalizedMonitors.map((monitor) => {
@ -81,9 +58,9 @@ export const syncNewMonitorBulk = async ({
try {
const [createdMonitors, [policiesResult, syncErrors]] = await Promise.all([
createNewSavedObjectMonitorBulk({
monitorsToCreate,
soClient: savedObjectsClient,
monitorConfigRepository.createBulk({
monitors: monitorsToCreate,
savedObjectType: query.savedObjectType,
}),
syntheticsMonitorClient.addMonitors(monitorsToCreate, privateLocations, spaceId),
]);
@ -182,12 +159,9 @@ export const deleteMonitorIfCreated = async ({
routeContext: RouteContext;
newMonitorId: string;
}) => {
const { server, savedObjectsClient } = routeContext;
const { server, monitorConfigRepository } = routeContext;
try {
const encryptedMonitor = await savedObjectsClient.get<EncryptedSyntheticsMonitorAttributes>(
syntheticsMonitorType,
newMonitorId
);
const encryptedMonitor = await monitorConfigRepository.get(newMonitorId);
if (encryptedMonitor) {
const deleteMonitorAPI = new DeleteMonitorAPI(routeContext);

View file

@ -7,7 +7,6 @@
import { SavedObject, SavedObjectsUpdateResponse } from '@kbn/core/server';
import { SavedObjectError } from '@kbn/core-saved-objects-common';
import { RouteContext } from '../../types';
import { syntheticsMonitorType } from '../../../../common/types/saved_objects';
import { FailedPolicyUpdate } from '../../../synthetics_service/private_location/synthetics_private_location';
import {
ConfigKey,
@ -82,9 +81,12 @@ 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({ monitors: data }),
monitorConfigRepository.bulkUpdate({
monitors: data,
}),
syncUpdatedMonitors({ monitorsToUpdate, routeContext, spaceId, privateLocations }),
]);
@ -132,17 +134,19 @@ export const rollbackCompletely = async ({
monitorsToUpdate: MonitorConfigUpdate[];
routeContext: RouteContext;
}) => {
const { savedObjectsClient, server } = routeContext;
const { server, monitorConfigRepository } = routeContext;
try {
await savedObjectsClient.bulkUpdate<MonitorFields>(
monitorsToUpdate.map(({ decryptedPreviousMonitor }) => ({
type: syntheticsMonitorType,
await monitorConfigRepository.bulkUpdate({
monitors: monitorsToUpdate.map(({ decryptedPreviousMonitor }) => ({
id: decryptedPreviousMonitor.id,
attributes: decryptedPreviousMonitor.attributes,
}))
);
attributes: decryptedPreviousMonitor.attributes as unknown as MonitorFields,
soType: decryptedPreviousMonitor.type,
})),
});
} catch (error) {
server.logger.error(`Unable to rollback Synthetics monitors edit ${error.message} `, { error });
server.logger.error(`Unable to rollback Synthetics monitors edit, Error: ${error.message}`, {
error,
});
}
};
@ -160,7 +164,7 @@ export const rollbackFailedUpdates = async ({
if (!failedPolicyUpdates || failedPolicyUpdates.length === 0) {
return;
}
const { server, savedObjectsClient } = routeContext;
const { server, monitorConfigRepository } = routeContext;
try {
const failedConfigs: Record<
@ -182,13 +186,13 @@ export const rollbackFailedUpdates = async ({
return failedConfigs[decryptedPreviousMonitor.id];
})
.map(({ decryptedPreviousMonitor }) => ({
type: syntheticsMonitorType,
id: decryptedPreviousMonitor.id,
attributes: decryptedPreviousMonitor.attributes,
attributes: decryptedPreviousMonitor.attributes as unknown as MonitorFields,
soType: decryptedPreviousMonitor.type,
}));
if (monitorsToRevert.length > 0) {
await savedObjectsClient.bulkUpdate<MonitorFields>(monitorsToRevert);
await monitorConfigRepository.bulkUpdate({ monitors: monitorsToRevert });
}
return failedConfigs;
} catch (error) {

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,13 +7,11 @@
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';
import { getMonitorNotFoundResponse } from '../synthetics_service/service_errors';
import { mapSavedObjectToMonitor } from './formatters/saved_object_to_monitor';
import { getSyntheticsMonitor } from '../../queries/get_monitor';
export const getSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () => ({
method: 'GET',
@ -36,9 +34,9 @@ export const getSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () => ({
handler: async ({
request,
response,
server: { encryptedSavedObjects, coreStart },
savedObjectsClient,
server: { coreStart },
spaceId,
monitorConfigRepository,
}): Promise<any> => {
const { monitorId } = request.params;
try {
@ -53,24 +51,21 @@ export const getSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () => ({
if (Boolean(canSave)) {
// only user with write permissions can decrypt the monitor
const encryptedSavedObjectsClient = encryptedSavedObjects.getClient();
const monitor = await getSyntheticsMonitor({
monitorId,
encryptedSavedObjectsClient,
const monitor = await monitorConfigRepository.getDecrypted(monitorId, spaceId);
return {
...mapSavedObjectToMonitor({ monitor: monitor.normalizedMonitor, internal }),
spaceId,
});
return { ...mapSavedObjectToMonitor({ monitor, 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,11 +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 { syntheticsMonitorType } from '../../../common/types/saved_objects';
import {
getMonitorFilters,
isMonitorsQueryFiltered,
MonitorsQuery,
parseMappingKey,
QuerySchema,
SEARCH_FIELDS,
} from '../common';
export const getAllSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () => ({
method: 'GET',
@ -20,19 +27,31 @@ export const getAllSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () =>
},
},
handler: async (routeContext): Promise<any> => {
const { request, savedObjectsClient, syntheticsMonitorClient } = routeContext;
const { request, syntheticsMonitorClient, monitorConfigRepository } = routeContext;
const totalCountQuery = async () => {
if (isMonitorsQueryFiltered(request.query)) {
return savedObjectsClient.find({
type: syntheticsMonitorType,
return monitorConfigRepository.find({
perPage: 0,
page: 1,
});
}
};
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(),
]);
@ -48,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({
...testICMPFields,
schedule: {
number: '4',
unit: ScheduleUnit.MINUTES,
},
} as unknown as MonitorFields);
const result = validateMonitor(
{
...testICMPFields,
schedule: {
number: '4',
unit: ScheduleUnit.MINUTES,
},
} 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({
...testICMPFields,
timeout: '3m',
} as unknown as MonitorFields);
const result = validateMonitor(
{
...testICMPFields,
timeout: '3m',
} 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({
...testICMPFields,
locations: ['invalid-location'],
} as unknown as MonitorFields);
const result = validateMonitor(
{
...testICMPFields,
locations: ['invalid-location'],
} 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({
...testMonitor,
alert: {},
});
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({
...testMonitor,
alert: {
// @ts-ignore
invalidKey: 'invalid',
enabled: true,
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/lib/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(),
}),
@ -65,13 +83,10 @@ export const addSyntheticsProjectMonitorRoute: SyntheticsRestApiRouteFactory = (
return response.forbidden({ body: { message: permissionError } });
}
const encryptedSavedObjectsClient = server.encryptedSavedObjects.getClient();
const pushMonitorFormatter = new ProjectMonitorFormatter({
routeContext,
projectId: decodedProjectName,
spaceId,
encryptedSavedObjectsClient,
monitors,
});

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 { 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, 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 = `${syntheticsMonitorType}.attributes.${
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}"`,
perPage,
sortField: ConfigKey.JOURNEY_ID,
sortOrder: 'asc',
searchAfter: decodedSearchAfter ? [...decodedSearchAfter.split(',')] : undefined,
},
},
},
{
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,
});
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
);
}
@ -133,7 +123,7 @@ export class DeleteMonitorAPI {
}: {
monitors: Array<SavedObject<SyntheticsMonitor | EncryptedSyntheticsMonitorAttributes>>;
}) {
const { savedObjectsClient, server, spaceId, syntheticsMonitorClient } = this.routeContext;
const { server, spaceId, syntheticsMonitorClient } = this.routeContext;
const { logger, telemetry, stackVersion } = server;
try {
@ -142,15 +132,14 @@ export class DeleteMonitorAPI {
...normalizedMonitor.attributes,
id: normalizedMonitor.attributes[ConfigKey.MONITOR_QUERY_ID],
})) as SyntheticsMonitorWithId[],
savedObjectsClient,
spaceId
);
const deletePromises = savedObjectsClient.bulkDelete(
monitors.map((monitor) => ({ type: syntheticsMonitorType, id: monitor.id }))
const deletePromise = this.routeContext.monitorConfigRepository.bulkDelete(
monitors.map((monitor) => ({ id: monitor.id, type: monitor.type }))
);
const [errors, result] = await Promise.all([deleteSyncPromise, deletePromises]);
const [errors, result] = await Promise.all([deleteSyncPromise, deletePromise]);
monitors.forEach((monitor) => {
sendTelemetryEvents(

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/typesWith
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),
@ -372,7 +373,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<{
key: string;
doc_count: number;
}>;
};
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);
handler: async ({ server, savedObjectsClient, syntheticsMonitorClient }) => {
const soClient = server.coreStart.savedObjects.createInternalRepository();
const { locations } = await getPrivateLocationsAndAgentPolicies(
savedObjectsClient,
syntheticsMonitorClient
);
const locationMonitors = await soClient.find({
type: syntheticsMonitorSOTypes,
perPage: 0,
aggs,
namespaces: [ALL_SPACES_ID],
});
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)
);
},
});
export const getMonitorsByLocation = async (server: SyntheticsServerSetup, locationId?: string) => {
const soClient = server.coreStart.savedObjects.createInternalRepository();
const locationFilter = getSavedObjectKqlFilter({ field: 'locations.id', values: locationId });
const locationMonitors = await soClient.find<unknown, ExpectedResponse>({
type: syntheticsMonitorType,
perPage: 0,
aggs,
filter: locationFilter,
namespaces: [ALL_SPACES_ID],
});
return (
locationMonitors.aggregations?.locations.buckets.map(({ key: id, doc_count: count }) => ({
id,
count,
})) ?? []
);
};

View file

@ -4,164 +4,3 @@
* 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 { savedObjectsClient } = route;
const { query } = route.request.query;
const { filtersStr } = await getMonitorFilters(route);
const { allLocations = [] } = await getAllLocations(route);
const data = await savedObjectsClient.find<EncryptedSyntheticsMonitorAttributes>({
type: syntheticsMonitorType,
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({
...fields,
[ConfigKey.METADATA]: updatedMetadata,
} as MonitorFields);
const validationResult = validateMonitor(
{
...fields,
[ConfigKey.METADATA]: updatedMetadata,
} 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,
};

View file

@ -7,7 +7,7 @@
import { SavedObjectsType } from '@kbn/core/server';
import { syntheticsParamType } from '../../common/types/saved_objects';
export const SYNTHETICS_SECRET_ENCRYPTED_TYPE = {
export const SYNTHETICS_PARAMS_SECRET_ENCRYPTED_TYPE = {
type: syntheticsParamType,
attributesToEncrypt: new Set(['value']),
attributesToIncludeInAAD: new Set(['key', 'description', 'tags']),

View file

@ -6,7 +6,11 @@
*/
import { i18n } from '@kbn/i18n';
import { SavedObjectsType } from '@kbn/core-saved-objects-server';
import { SavedObjectsErrorHelpers, SavedObjectsType } from '@kbn/core-saved-objects-server';
import { SavedObjectsClientContract } from '@kbn/core-saved-objects-api-server';
import { DynamicSettingsAttributes } from '../runtime_types/settings';
import { fromSettingsAttribute } from '../routes/settings/dynamic_settings';
import { DYNAMIC_SETTINGS_DEFAULT_ATTRIBUTES } from '../constants/settings';
export const uptimeSettingsObjectType = 'uptime-dynamic-settings';
export const uptimeSettingsObjectId = 'uptime-dynamic-settings-singleton';
@ -31,3 +35,48 @@ export const syntheticsSettings: SavedObjectsType = {
}),
},
};
export const 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;
}
};
export const 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;
}
};

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