diff --git a/packages/kbn-check-mappings-update-cli/current_fields.json b/packages/kbn-check-mappings-update-cli/current_fields.json index 1d83dc6e5897..814f3404fd66 100644 --- a/packages/kbn-check-mappings-update-cli/current_fields.json +++ b/packages/kbn-check-mappings-update-cli/current_fields.json @@ -1159,6 +1159,34 @@ "type", "urls" ], + "synthetics-monitor-multi-space": [ + "alert", + "alert.status", + "alert.status.enabled", + "alert.tls", + "alert.tls.enabled", + "config_id", + "custom_heartbeat_id", + "enabled", + "hash", + "hosts", + "id", + "journey_id", + "locations", + "locations.id", + "locations.label", + "maintenance_windows", + "name", + "origin", + "project_id", + "schedule", + "schedule.number", + "tags", + "throttling", + "throttling.label", + "type", + "urls" + ], "synthetics-param": [], "synthetics-private-location": [], "synthetics-privates-locations": [], diff --git a/packages/kbn-check-mappings-update-cli/current_mappings.json b/packages/kbn-check-mappings-update-cli/current_mappings.json index 7a3811041a1b..7378ec08bd0d 100644 --- a/packages/kbn-check-mappings-update-cli/current_mappings.json +++ b/packages/kbn-check-mappings-update-cli/current_mappings.json @@ -3832,6 +3832,136 @@ } } }, + "synthetics-monitor-multi-space": { + "dynamic": false, + "properties": { + "alert": { + "properties": { + "status": { + "properties": { + "enabled": { + "type": "boolean" + } + } + }, + "tls": { + "properties": { + "enabled": { + "type": "boolean" + } + } + } + } + }, + "config_id": { + "type": "keyword" + }, + "custom_heartbeat_id": { + "type": "keyword" + }, + "enabled": { + "type": "boolean" + }, + "hash": { + "type": "keyword" + }, + "hosts": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "id": { + "type": "keyword" + }, + "journey_id": { + "type": "keyword" + }, + "locations": { + "properties": { + "id": { + "fields": { + "text": { + "type": "text" + } + }, + "ignore_above": 256, + "type": "keyword" + }, + "label": { + "type": "text" + } + } + }, + "maintenance_windows": { + "type": "keyword" + }, + "name": { + "fields": { + "keyword": { + "ignore_above": 256, + "normalizer": "lowercase", + "type": "keyword" + } + }, + "type": "text" + }, + "origin": { + "type": "keyword" + }, + "project_id": { + "fields": { + "text": { + "type": "text" + } + }, + "type": "keyword" + }, + "schedule": { + "properties": { + "number": { + "type": "integer" + } + } + }, + "tags": { + "fields": { + "text": { + "type": "text" + } + }, + "type": "keyword" + }, + "throttling": { + "properties": { + "label": { + "type": "keyword" + } + } + }, + "type": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "urls": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + } + } + }, "synthetics-param": { "dynamic": false, "properties": {} diff --git a/src/core/packages/saved-objects/server-internal/src/object_types/index.ts b/src/core/packages/saved-objects/server-internal/src/object_types/index.ts index 1e7f1aef4002..a710eb103adb 100644 --- a/src/core/packages/saved-objects/server-internal/src/object_types/index.ts +++ b/src/core/packages/saved-objects/server-internal/src/object_types/index.ts @@ -11,4 +11,4 @@ export { registerCoreObjectTypes } from './registration'; // set minimum number of registered saved objects to ensure no object types are removed after 8.8 // declared in internal implementation explicitly to prevent unintended changes. -export const SAVED_OBJECT_TYPES_COUNT = 135 as const; +export const SAVED_OBJECT_TYPES_COUNT = 136 as const; diff --git a/src/core/server/integration_tests/ci_checks/saved_objects/check_registered_types.test.ts b/src/core/server/integration_tests/ci_checks/saved_objects/check_registered_types.test.ts index cf82ab810e31..532958331986 100644 --- a/src/core/server/integration_tests/ci_checks/saved_objects/check_registered_types.test.ts +++ b/src/core/server/integration_tests/ci_checks/saved_objects/check_registered_types.test.ts @@ -177,6 +177,7 @@ describe('checking migration metadata changes on all registered SO types', () => "spaces-usage-stats": "084bd0f080f94fb5735d7f3cf12f13ec92f36bad", "synthetics-dynamic-settings": "7804b079cc502f16526f7c9491d1397cc1ec67db", "synthetics-monitor": "fdebfa2449d2b934972d1743dc78c34ae9ebc9c1", + "synthetics-monitor-multi-space": "c8c9dab447ba8a7383041f55ba80757365d114c5", "synthetics-param": "9776c9b571d35f0d0397e8915e035ea1dc026db7", "synthetics-private-location": "27aaa44f792f70b734905e44e3e9b56bbeac7b86", "synthetics-privates-locations": "36036b881524108c7327fe14bd224c6e4d972cb5", diff --git a/src/core/server/integration_tests/saved_objects/registration/type_registrations.test.ts b/src/core/server/integration_tests/saved_objects/registration/type_registrations.test.ts index ce87be49ce7e..252389993955 100644 --- a/src/core/server/integration_tests/saved_objects/registration/type_registrations.test.ts +++ b/src/core/server/integration_tests/saved_objects/registration/type_registrations.test.ts @@ -149,6 +149,7 @@ const previouslyRegisteredTypes = [ 'space', 'spaces-usage-stats', 'synthetics-monitor', + 'synthetics-monitor-multi-space', 'synthetics-param', 'synthetics-privates-locations', 'synthetics-private-location', diff --git a/src/platform/packages/shared/kbn-test/src/kbn_client/kbn_client_saved_objects.ts b/src/platform/packages/shared/kbn-test/src/kbn_client/kbn_client_saved_objects.ts index 0b6ba0be80fa..620572ba23de 100644 --- a/src/platform/packages/shared/kbn-test/src/kbn_client/kbn_client_saved_objects.ts +++ b/src/platform/packages/shared/kbn-test/src/kbn_client/kbn_client_saved_objects.ts @@ -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', diff --git a/x-pack/platform/plugins/private/translations/translations/fr-FR.json b/x-pack/platform/plugins/private/translations/translations/fr-FR.json index afcf3d95bdf0..71e22363698b 100644 --- a/x-pack/platform/plugins/private/translations/translations/fr-FR.json +++ b/x-pack/platform/plugins/private/translations/translations/fr-FR.json @@ -45848,7 +45848,6 @@ "xpack.synthetics.syntheticsEmbeddable.monitors.ariaLabel": "Aperçu des moniteurs", "xpack.synthetics.syntheticsEmbeddable.stats.ariaLabel": "Statistiques des moniteurs", "xpack.synthetics.syntheticsFeatureCatalogueTitle": "Synthetics", - "xpack.synthetics.syntheticsMonitors.label": "Synthetics - Moniteur", "xpack.synthetics.tableTitle.showing": "Affichage de {count} sur {total} {label}", "xpack.synthetics.tagsSelectPlaceholder": "Sélectionner des balises", "xpack.synthetics.testDetails.after": "Après", diff --git a/x-pack/platform/plugins/private/translations/translations/ja-JP.json b/x-pack/platform/plugins/private/translations/translations/ja-JP.json index fdda24050cc3..4e06479f9b0d 100644 --- a/x-pack/platform/plugins/private/translations/translations/ja-JP.json +++ b/x-pack/platform/plugins/private/translations/translations/ja-JP.json @@ -45899,7 +45899,6 @@ "xpack.synthetics.syntheticsEmbeddable.monitors.ariaLabel": "モニター概要", "xpack.synthetics.syntheticsEmbeddable.stats.ariaLabel": "モニター統計", "xpack.synthetics.syntheticsFeatureCatalogueTitle": "Synthetics", - "xpack.synthetics.syntheticsMonitors.label": "Synthetics - 監視", "xpack.synthetics.tableTitle.showing": "{count}/{total} {label}を表示中", "xpack.synthetics.tagsSelectPlaceholder": "タグを選択", "xpack.synthetics.testDetails.after": "後", diff --git a/x-pack/platform/plugins/private/translations/translations/zh-CN.json b/x-pack/platform/plugins/private/translations/translations/zh-CN.json index bcc5d8f1d538..4646d5e35670 100644 --- a/x-pack/platform/plugins/private/translations/translations/zh-CN.json +++ b/x-pack/platform/plugins/private/translations/translations/zh-CN.json @@ -45875,7 +45875,6 @@ "xpack.synthetics.syntheticsEmbeddable.monitors.ariaLabel": "监测概览", "xpack.synthetics.syntheticsEmbeddable.stats.ariaLabel": "监测统计信息", "xpack.synthetics.syntheticsFeatureCatalogueTitle": "Synthetics", - "xpack.synthetics.syntheticsMonitors.label": "Synthetics - 监测", "xpack.synthetics.tableTitle.showing": "正在显示 {count} 个(共 {total} 个){label}", "xpack.synthetics.tagsSelectPlaceholder": "选择标签", "xpack.synthetics.testDetails.after": "之后", diff --git a/x-pack/solutions/observability/plugins/observability_shared/public/components/tags_list/tags_list.tsx b/x-pack/solutions/observability/plugins/observability_shared/public/components/tags_list/tags_list.tsx index 7997157a51b0..a7aadbf93c55 100644 --- a/x-pack/solutions/observability/plugins/observability_shared/public/components/tags_list/tags_list.tsx +++ b/x-pack/solutions/observability/plugins/observability_shared/public/components/tags_list/tags_list.tsx @@ -52,7 +52,7 @@ const TagsList = ({ const tagsToDisplay = tags.slice(0, toDisplay); return ( - + {tagsToDisplay.map((tag) => ( // filtering only makes sense in monitor list, where we have summary diff --git a/x-pack/solutions/observability/plugins/synthetics/common/constants/monitor_defaults.ts b/x-pack/solutions/observability/plugins/synthetics/common/constants/monitor_defaults.ts index cea68ee17c53..dbef4bf55fd3 100644 --- a/x-pack/solutions/observability/plugins/synthetics/common/constants/monitor_defaults.ts +++ b/x-pack/solutions/observability/plugins/synthetics/common/constants/monitor_defaults.ts @@ -155,6 +155,7 @@ export const DEFAULT_COMMON_FIELDS: CommonFields = { [ConfigKey.LABELS]: {}, [ConfigKey.MAX_ATTEMPTS]: 2, [ConfigKey.MAINTENANCE_WINDOWS]: [], + [ConfigKey.KIBANA_SPACES]: [], revision: 1, }; diff --git a/x-pack/solutions/observability/plugins/synthetics/common/constants/monitor_management.ts b/x-pack/solutions/observability/plugins/synthetics/common/constants/monitor_management.ts index 4ba5800ee969..53585ffb0932 100644 --- a/x-pack/solutions/observability/plugins/synthetics/common/constants/monitor_management.ts +++ b/x-pack/solutions/observability/plugins/synthetics/common/constants/monitor_management.ts @@ -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 = [ diff --git a/x-pack/solutions/observability/plugins/synthetics/common/runtime_types/monitor_management/monitor_types.ts b/x-pack/solutions/observability/plugins/synthetics/common/runtime_types/monitor_management/monitor_types.ts index 7bba176174b8..faf91bb5796f 100644 --- a/x-pack/solutions/observability/plugins/synthetics/common/runtime_types/monitor_management/monitor_types.ts +++ b/x-pack/solutions/observability/plugins/synthetics/common/runtime_types/monitor_management/monitor_types.ts @@ -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)])), }), ]); diff --git a/x-pack/solutions/observability/plugins/synthetics/common/runtime_types/monitor_management/synthetics_overview_status.ts b/x-pack/solutions/observability/plugins/synthetics/common/runtime_types/monitor_management/synthetics_overview_status.ts index cce5b1e31a2c..eee5d4f91fe8 100644 --- a/x-pack/solutions/observability/plugins/synthetics/common/runtime_types/monitor_management/synthetics_overview_status.ts +++ b/x-pack/solutions/observability/plugins/synthetics/common/runtime_types/monitor_management/synthetics_overview_status.ts @@ -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), }), diff --git a/x-pack/solutions/observability/plugins/synthetics/common/types/saved_objects.ts b/x-pack/solutions/observability/plugins/synthetics/common/types/saved_objects.ts index 4e0992f78ee0..56decfd457a5 100644 --- a/x-pack/solutions/observability/plugins/synthetics/common/types/saved_objects.ts +++ b/x-pack/solutions/observability/plugins/synthetics/common/types/saved_objects.ts @@ -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, +]; diff --git a/x-pack/solutions/observability/plugins/synthetics/common/utils/date_util.ts b/x-pack/solutions/observability/plugins/synthetics/common/utils/date_util.ts new file mode 100644 index 000000000000..af36c4ea5614 --- /dev/null +++ b/x-pack/solutions/observability/plugins/synthetics/common/utils/date_util.ts @@ -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'); + } +}; diff --git a/x-pack/solutions/observability/plugins/synthetics/e2e/synthetics/journeys/project_monitor_read_only.journey.ts b/x-pack/solutions/observability/plugins/synthetics/e2e/synthetics/journeys/project_monitor_read_only.journey.ts index edef44a21d05..871663c68e79 100644 --- a/x-pack/solutions/observability/plugins/synthetics/e2e/synthetics/journeys/project_monitor_read_only.journey.ts +++ b/x-pack/solutions/observability/plugins/synthetics/e2e/synthetics/journeys/project_monitor_read_only.journey.ts @@ -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'] ) ); }); diff --git a/x-pack/solutions/observability/plugins/synthetics/e2e/synthetics/journeys/services/add_monitor.ts b/x-pack/solutions/observability/plugins/synthetics/e2e/synthetics/journeys/services/add_monitor.ts index 6a527da275eb..41a972121a1b 100644 --- a/x-pack/solutions/observability/plugins/synthetics/e2e/synthetics/journeys/services/add_monitor.ts +++ b/x-pack/solutions/observability/plugins/synthetics/e2e/synthetics/journeys/services/add_monitor.ts @@ -48,7 +48,9 @@ export const cleanTestMonitors = async (params: Record) => { 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); diff --git a/x-pack/solutions/observability/plugins/synthetics/e2e/synthetics/journeys/services/synthetics_services.ts b/x-pack/solutions/observability/plugins/synthetics/e2e/synthetics/journeys/services/synthetics_services.ts index f8b8b1a2d97b..13ccc1e1896b 100644 --- a/x-pack/solutions/observability/plugins/synthetics/e2e/synthetics/journeys/services/synthetics_services.ts +++ b/x-pack/solutions/observability/plugins/synthetics/e2e/synthetics/journeys/services/synthetics_services.ts @@ -202,7 +202,9 @@ export class SyntheticsServices { const getService = this.params.getService; const server = getService('kibanaServer'); - await server.savedObjects.clean({ types: ['synthetics-monitor', 'alert'] }); + await server.savedObjects.clean({ + types: ['synthetics-monitor', 'synthetics-monitor-multi-space', 'alert'], + }); await this.cleanUpAlerts(); } catch (e) { // eslint-disable-next-line no-console diff --git a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/fields/monitor_spaces.test.ts b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/fields/monitor_spaces.test.ts new file mode 100644 index 000000000000..dd8d6941eabc --- /dev/null +++ b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/fields/monitor_spaces.test.ts @@ -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([]); + }); +}); diff --git a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/fields/monitor_spaces.tsx b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/fields/monitor_spaces.tsx new file mode 100644 index 000000000000..70805eb05d4a --- /dev/null +++ b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/fields/monitor_spaces.tsx @@ -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(); + const [spacesList, setSpacesList] = React.useState>([]); + 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 ( + + 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 ', +}); diff --git a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/field_config.tsx b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/field_config.tsx index cb4901a1a814..3dec4d46f0dd 100644 --- a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/field_config.tsx +++ b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/field_config.tsx @@ -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: , }, + [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); + }, + }), + }, }); diff --git a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/field_wrappers.tsx b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/field_wrappers.tsx index 8d66289ac187..bf279535b620 100644 --- a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/field_wrappers.tsx +++ b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/field_wrappers.tsx @@ -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) => ); + +export const KibanaSpacesWrapper = React.forwardRef((props, _ref) => ( + +)); diff --git a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/form_config.tsx b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/form_config.tsx index d08463e2d48d..75b29af7bf21 100644 --- a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/form_config.tsx +++ b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/form_config.tsx @@ -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), ], }, }); diff --git a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/index.tsx b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/index.tsx index 103e523935e6..6ac2a5006e38 100644 --- a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/index.tsx +++ b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/index.tsx @@ -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 = ({ children }) => <>{children}; export const MonitorForm: FC< PropsWithChildren<{ @@ -31,6 +35,14 @@ export const MonitorForm: FC< shouldFocusError: false, }); + const { spaces: spacesApi } = useKibana().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 ( - - - {children} - - - - - + + + + {children} + + + + + + ); }; diff --git a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/monitor_edit_page.test.tsx b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/monitor_edit_page.test.tsx index 6c4c7d5103c1..df4c9c89363b 100644 --- a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/monitor_edit_page.test.tsx +++ b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/monitor_edit_page.test.tsx @@ -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; diff --git a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/types.ts b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/types.ts index f784894b469f..c2a0475b0cc6 100644 --- a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/types.ts +++ b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/types.ts @@ -168,4 +168,5 @@ export interface FieldMap { [ConfigKey.MAX_ATTEMPTS]: FieldMeta; [ConfigKey.LABELS]: FieldMeta; [ConfigKey.MAINTENANCE_WINDOWS]: FieldMeta; + [ConfigKey.KIBANA_SPACES]: FieldMeta; } diff --git a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/monitor_list_table/columns.tsx b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/monitor_list_table/columns.tsx index c5b0bbf146dc..628fa1f8775d 100644 --- a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/monitor_list_table/columns.tsx +++ b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/monitor_list_table/columns.tsx @@ -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> { const history = useHistory(); - const { http } = useKibana().services; + const { http, spaces } = useKibana().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> = [ { @@ -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 ( + + ); + }, + }, { 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]}`)!; diff --git a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/monitor_list_table/monitor_list.tsx b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/monitor_list_table/monitor_list.tsx index 03835881a2c5..cbaae45f9e89 100644 --- a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/monitor_list_table/monitor_list.tsx +++ b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/monitor_list_table/monitor_list.tsx @@ -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 = ({ children }) => <>{children}; export const MonitorList = ({ pageState: { pageIndex, pageSize, sortField, sortOrder }, @@ -108,9 +112,16 @@ export const MonitorList = ({ onSelectionChange, initialSelected: selectedItems, }; + const { spaces: spacesApi } = useKibana().services; + + const ContextWrapper = useMemo( + () => + spacesApi ? spacesApi.ui.components.getSpacesContextProvider : getEmptyFunctionComponent, + [spacesApi] + ); return ( - <> + )} - + ); }; diff --git a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/actions_popover.tsx b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/actions_popover.tsx index 3f126bfb2fcf..a7a27b211b7a 100644 --- a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/actions_popover.tsx +++ b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/actions_popover.tsx @@ -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(); diff --git a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/compact_view/components/monitor_status_col.tsx b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/compact_view/components/monitor_status_col.tsx new file mode 100644 index 000000000000..e90c16da2df9 --- /dev/null +++ b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/compact_view/components/monitor_status_col.tsx @@ -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 ( +
+ openFlyout(monitor)} + /> + + {timestamp ? ( + + + {timestamp.fromNow()} + + + + {timestamp.toLocaleString()} + + + } + > + + {getCheckedLabel(timestamp)} + + + ) : ( + '--' + )} +
+ ); +}; + +const getCheckedLabel = (timestamp: Moment) => { + return i18n.translate('xpack.synthetics.monitorList.statusColumn.checkedTimestamp', { + defaultMessage: 'Checked {timestamp}', + values: { timestamp: getShortTimeStamp(timestamp) }, + }); +}; diff --git a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/compact_view/components/monitors_table.tsx b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/compact_view/components/monitors_table.tsx index 9c1956badf9e..227d983aa972 100644 --- a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/compact_view/components/monitors_table.tsx +++ b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/compact_view/components/monitors_table.tsx @@ -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, }) ); } diff --git a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/compact_view/hooks/use_monitors_table_columns.tsx b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/compact_view/hooks/use_monitors_table_columns.tsx index 7d70426fe144..43fdaf119ada 100644 --- a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/compact_view/hooks/use_monitors_table_columns.tsx +++ b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/compact_view/hooks/use_monitors_table_columns.tsx @@ -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().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> = useMemo( - () => [ + const columns: Array> = useMemo(() => { + const LazySpaceList = spaces?.ui.components.getSpaceList ?? (() => null); + + return [ { - field: 'status', name: STATUS, - render: (status: OverviewStatusMetaData['status'], monitor) => ( - openFlyout(monitor)} - /> + render: (monitor: OverviewStatusMetaData) => ( + ), }, { @@ -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 ( + + ); + }, + }, + ] + : []), { name: ACTIONS, render: (monitor: OverviewStatusMetaData) => , align: 'right', width: '40px', }, - ], - [histogramsById, minInterval, onClickMonitorFilter, openFlyout] - ); + ]; + }, [ + histogramsById, + minInterval, + onClickMonitorFilter, + openFlyout, + showFromAllSpaces, + space, + spaces?.ui.components.getSpaceList, + ]); return { columns, diff --git a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/metric_item/metric_item.tsx b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/metric_item/metric_item.tsx index cf965e4da00e..3786eb1769e6 100644 --- a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/metric_item/metric_item.tsx +++ b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/metric_item/metric_item.tsx @@ -33,7 +33,7 @@ import { MetricItemExtra } from './metric_item_extra'; import { MetricItemIcon } from './metric_item_icon'; import { FlyoutParamProps } from '../types'; -const METRIC_ITEM_HEIGHT = 160; +const METRIC_ITEM_HEIGHT = 170; export const getColor = (euiTheme: EuiThemeComputed, isEnabled: boolean, status?: string) => { if (!isEnabled) { @@ -171,7 +171,7 @@ export const MetricItem = ({ id: monitor.configId, location: locationName, locationId: monitor.locationId, - spaceId: monitor.spaceId, + spaces: monitor.spaces, }); } }} diff --git a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/monitor_detail_flyout.tsx b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/monitor_detail_flyout.tsx index 923c89a64871..5e2decf3432b 100644 --- a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/monitor_detail_flyout.tsx +++ b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/monitor_detail_flyout.tsx @@ -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} diff --git a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/overview_grid.tsx b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/overview_grid.tsx index 40875561a1a3..2641953d0dac 100644 --- a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/overview_grid.tsx +++ b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/overview_grid.tsx @@ -44,7 +44,7 @@ import { MaybeMonitorDetailsFlyout } from './monitor_detail_flyout'; import { OverviewGridCompactView } from './compact_view/overview_grid_compact_view'; import { ViewButtons } from './view_buttons/view_buttons'; -const ITEM_HEIGHT = 172; +const ITEM_HEIGHT = 182; const ROW_COUNT = 4; const MAX_LIST_HEIGHT = 800; const MIN_BATCH_SIZE = 20; @@ -189,7 +189,7 @@ export const OverviewGrid = memo( {listData[listIndex].map((_, idx) => ( ({ const showFieldInvalid = (isSubmitted || isTouched) && !!error; useEffect(() => { - if (data) { + if (data?.spacesDataPromise) { data.spacesDataPromise.then((spacesData) => { setSpacesList([ allSpacesOption, diff --git a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/contexts/synthetics_shared_context.tsx b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/contexts/synthetics_shared_context.tsx index 78f6ce2d0277..cc0284550354 100644 --- a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/contexts/synthetics_shared_context.tsx +++ b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/contexts/synthetics_shared_context.tsx @@ -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 = ({ children }) => <>{children}; export const SyntheticsSharedContext: React.FC< React.PropsWithChildren; 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 ( - {children} + {children} diff --git a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/hooks/use_edit_monitor_locator.ts b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/hooks/use_edit_monitor_locator.ts index 43492ec72243..e5d5ca6a8223 100644 --- a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/hooks/use_edit_monitor_locator.ts +++ b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/hooks/use_edit_monitor_locator.ts @@ -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; } diff --git a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/hooks/use_monitor_detail_locator.ts b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/hooks/use_monitor_detail_locator.ts index 3b98a6e60279..6758e14ce6d2 100644 --- a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/hooks/use_monitor_detail_locator.ts +++ b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/hooks/use_monitor_detail_locator.ts @@ -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(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; } diff --git a/x-pack/solutions/observability/plugins/synthetics/public/utils/api_service/api_service.ts b/x-pack/solutions/observability/plugins/synthetics/public/utils/api_service/api_service.ts index e004eddb3023..fd576e14bffd 100644 --- a/x-pack/solutions/observability/plugins/synthetics/public/utils/api_service/api_service.ts +++ b/x-pack/solutions/observability/plugins/synthetics/public/utils/api_service/api_service.ts @@ -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); } diff --git a/x-pack/solutions/observability/plugins/synthetics/server/alert_rules/status_rule/queries/filter_monitors.ts b/x-pack/solutions/observability/plugins/synthetics/server/alert_rules/status_rule/queries/filter_monitors.ts index 0aa6f43ab747..dfbe420ce208 100644 --- a/x-pack/solutions/observability/plugins/synthetics/server/alert_rules/status_rule/queries/filter_monitors.ts +++ b/x-pack/solutions/observability/plugins/synthetics/server/alert_rules/status_rule/queries/filter_monitors.ts @@ -8,6 +8,7 @@ import { KueryNode, fromKueryExpression, toElasticsearchQuery } from '@kbn/es-query'; import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types'; import { SyntheticsMonitorStatusRuleParams as StatusRuleParams } from '@kbn/response-ops-rule-params/synthetics_monitor_status'; +import { ALL_SPACES_ID } from '@kbn/security-plugin/common/constants'; import { SyntheticsEsClient } from '../../../lib'; import { FINAL_SUMMARY_FILTER, @@ -48,8 +49,8 @@ export async function queryFilterMonitors({ getRangeFilter({ from: 'now-24h/m', to: 'now/m' }), getTimeSpanFilter(), { - term: { - 'meta.space_id': spaceId, + terms: { + 'meta.space_id': [spaceId, ALL_SPACES_ID], }, }, { diff --git a/x-pack/solutions/observability/plugins/synthetics/server/alert_rules/status_rule/queries/helpers.ts b/x-pack/solutions/observability/plugins/synthetics/server/alert_rules/status_rule/queries/helpers.ts index eee2b527dafb..b6063dd294ec 100644 --- a/x-pack/solutions/observability/plugins/synthetics/server/alert_rules/status_rule/queries/helpers.ts +++ b/x-pack/solutions/observability/plugins/synthetics/server/alert_rules/status_rule/queries/helpers.ts @@ -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; diff --git a/x-pack/solutions/observability/plugins/synthetics/server/alert_rules/status_rule/queries/query_monitor_status_alert.ts b/x-pack/solutions/observability/plugins/synthetics/server/alert_rules/status_rule/queries/query_monitor_status_alert.ts index 54a5954ce6a0..cafc0d3e62fe 100644 --- a/x-pack/solutions/observability/plugins/synthetics/server/alert_rules/status_rule/queries/query_monitor_status_alert.ts +++ b/x-pack/solutions/observability/plugins/synthetics/server/alert_rules/status_rule/queries/query_monitor_status_alert.ts @@ -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, diff --git a/x-pack/solutions/observability/plugins/synthetics/server/alert_rules/status_rule/status_rule_executor.test.ts b/x-pack/solutions/observability/plugins/synthetics/server/alert_rules/status_rule/status_rule_executor.test.ts index 001ae2be76a1..dfd5ed41feb3 100644 --- a/x-pack/solutions/observability/plugins/synthetics/server/alert_rules/status_rule/status_rule_executor.test.ts +++ b/x-pack/solutions/observability/plugins/synthetics/server/alert_rules/status_rule/status_rule_executor.test.ts @@ -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', }); }); diff --git a/x-pack/solutions/observability/plugins/synthetics/server/alert_rules/status_rule/status_rule_executor.ts b/x-pack/solutions/observability/plugins/synthetics/server/alert_rules/status_rule/status_rule_executor.ts index 930bf412afed..bfaf145b976f 100644 --- a/x-pack/solutions/observability/plugins/synthetics/server/alert_rules/status_rule/status_rule_executor.ts +++ b/x-pack/solutions/observability/plugins/synthetics/server/alert_rules/status_rule/status_rule_executor.ts @@ -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({ diff --git a/x-pack/solutions/observability/plugins/synthetics/server/alert_rules/tls_rule/tls_rule_executor.test.ts b/x-pack/solutions/observability/plugins/synthetics/server/alert_rules/tls_rule/tls_rule_executor.test.ts index ab422d403a03..c8c3e21b605c 100644 --- a/x-pack/solutions/observability/plugins/synthetics/server/alert_rules/tls_rule/tls_rule_executor.test.ts +++ b/x-pack/solutions/observability/plugins/synthetics/server/alert_rules/tls_rule/tls_rule_executor.test.ts @@ -62,7 +62,7 @@ describe('tlsRuleExecutor', () => { const monitorClient = new SyntheticsMonitorClient(syntheticsService, serverMock); const commonFilter = - 'synthetics-monitor.attributes.alert.tls.enabled: true and (synthetics-monitor.attributes.type: http or synthetics-monitor.attributes.type: tcp)'; + 'synthetics-monitor-multi-space.attributes.alert.tls.enabled: true and (synthetics-monitor-multi-space.attributes.type: http or synthetics-monitor-multi-space.attributes.type: tcp)'; const getTLSRuleExecutorParams = ( ruleParams: TLSRuleParams = {} @@ -110,7 +110,7 @@ describe('tlsRuleExecutor', () => { await tlsRule.getMonitors(); expect(getAllMock).toHaveBeenCalledWith({ - filter: `${commonFilter} AND synthetics-monitor.attributes.id:(\"${monitorId}\")`, + filter: `${commonFilter} AND synthetics-monitor-multi-space.attributes.id:(\"${monitorId}\")`, }); }); @@ -123,7 +123,7 @@ describe('tlsRuleExecutor', () => { await tlsRule.getMonitors(); expect(getAllMock).toHaveBeenCalledWith({ - filter: `${commonFilter} AND synthetics-monitor.attributes.tags:(\"${tag}\")`, + filter: `${commonFilter} AND synthetics-monitor-multi-space.attributes.tags:(\"${tag}\")`, }); }); @@ -138,7 +138,7 @@ describe('tlsRuleExecutor', () => { await tlsRule.getMonitors(); expect(getAllMock).toHaveBeenCalledWith({ - filter: `${commonFilter} AND synthetics-monitor.attributes.type:(\"${monitorType}\")`, + filter: `${commonFilter} AND synthetics-monitor-multi-space.attributes.type:(\"${monitorType}\")`, }); }); diff --git a/x-pack/solutions/observability/plugins/synthetics/server/alert_rules/tls_rule/tls_rule_executor.ts b/x-pack/solutions/observability/plugins/synthetics/server/alert_rules/tls_rule/tls_rule_executor.ts index 881caf671c3e..c461e0a5e238 100644 --- a/x-pack/solutions/observability/plugins/synthetics/server/alert_rules/tls_rule/tls_rule_executor.ts +++ b/x-pack/solutions/observability/plugins/synthetics/server/alert_rules/tls_rule/tls_rule_executor.ts @@ -14,15 +14,16 @@ import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types'; import type { TLSRuleParams } from '@kbn/response-ops-rule-params/synthetics_tls'; import moment from 'moment'; import { isEmpty } from 'lodash'; +import { getSyntheticsDynamicSettings } from '../../saved_objects/synthetics_settings'; +import { syntheticsMonitorAttributes } from '../../../common/types/saved_objects'; import { TLSRuleInspect } from '../../../common/runtime_types/alert_rules/common'; import { MonitorConfigRepository } from '../../services/monitor_config_repository'; import { FINAL_SUMMARY_FILTER } from '../../../common/constants/client_defaults'; import { formatFilterString } from '../common'; import { SyntheticsServerSetup } from '../../types'; import { getSyntheticsCerts } from '../../queries/get_certs'; -import { savedObjectsAdapter } from '../../saved_objects'; import { DYNAMIC_SETTINGS_DEFAULTS, SYNTHETICS_INDEX_PATTERN } from '../../../common/constants'; -import { processMonitors } from '../../saved_objects/synthetics_monitor/get_all_monitors'; +import { processMonitors } from '../../saved_objects/synthetics_monitor/process_monitors'; import { CertResult, ConfigKey, @@ -30,7 +31,6 @@ import { Ping, } from '../../../common/runtime_types'; import { SyntheticsMonitorClient } from '../../synthetics_service/synthetics_monitor/synthetics_monitor_client'; -import { monitorAttributes } from '../../../common/types/saved_objects'; import { AlertConfigKey } from '../../../common/constants/monitor_management'; import { SyntheticsEsClient } from '../../lib'; import { queryFilterMonitors } from '../status_rule/queries/filter_monitors'; @@ -81,9 +81,9 @@ export class TLSRuleExecutor { } async getMonitors() { - const HTTP_OR_TCP = `${monitorAttributes}.${ConfigKey.MONITOR_TYPE}: http or ${monitorAttributes}.${ConfigKey.MONITOR_TYPE}: tcp`; + const HTTP_OR_TCP = `${syntheticsMonitorAttributes}.${ConfigKey.MONITOR_TYPE}: http or ${syntheticsMonitorAttributes}.${ConfigKey.MONITOR_TYPE}: tcp`; - const baseFilter = `${monitorAttributes}.${AlertConfigKey.TLS_ENABLED}: true and (${HTTP_OR_TCP})`; + const baseFilter = `${syntheticsMonitorAttributes}.${AlertConfigKey.TLS_ENABLED}: true and (${HTTP_OR_TCP})`; const configIds = await queryFilterMonitors({ spaceId: this.spaceId, @@ -135,7 +135,7 @@ export class TLSRuleExecutor { async getExpiredCertificates() { const { enabledMonitorQueryIds } = await this.getMonitors(); - const dynamicSettings = await savedObjectsAdapter.getSyntheticsDynamicSettings(this.soClient); + const dynamicSettings = await getSyntheticsDynamicSettings(this.soClient); const expiryThreshold = this.params.certExpirationThreshold ?? diff --git a/x-pack/solutions/observability/plugins/synthetics/server/feature.ts b/x-pack/solutions/observability/plugins/synthetics/server/feature.ts index d0fc60c674dd..222942446d67 100644 --- a/x-pack/solutions/observability/plugins/synthetics/server/feature.ts +++ b/x-pack/solutions/observability/plugins/synthetics/server/feature.ts @@ -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, diff --git a/x-pack/solutions/observability/plugins/synthetics/server/mocks/route_context_mock.ts b/x-pack/solutions/observability/plugins/synthetics/server/mocks/route_context_mock.ts new file mode 100644 index 000000000000..50de24c040b1 --- /dev/null +++ b/x-pack/solutions/observability/plugins/synthetics/server/mocks/route_context_mock.ts @@ -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, + }; +}; diff --git a/x-pack/solutions/observability/plugins/synthetics/server/mocks/server_mock.ts b/x-pack/solutions/observability/plugins/synthetics/server/mocks/server_mock.ts new file mode 100644 index 000000000000..ccbfc22c6afc --- /dev/null +++ b/x-pack/solutions/observability/plugins/synthetics/server/mocks/server_mock.ts @@ -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; +}; diff --git a/x-pack/solutions/observability/plugins/synthetics/server/routes/certs/get_certificates.test.ts b/x-pack/solutions/observability/plugins/synthetics/server/routes/certs/get_certificates.test.ts index a477dba80d63..da8fd336ec14 100644 --- a/x-pack/solutions/observability/plugins/synthetics/server/routes/certs/get_certificates.test.ts +++ b/x-pack/solutions/observability/plugins/synthetics/server/routes/certs/get_certificates.test.ts @@ -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'; diff --git a/x-pack/solutions/observability/plugins/synthetics/server/routes/certs/get_certificates.ts b/x-pack/solutions/observability/plugins/synthetics/server/routes/certs/get_certificates.ts index 23f44e32bb15..2e0047cd86b1 100644 --- a/x-pack/solutions/observability/plugins/synthetics/server/routes/certs/get_certificates.ts +++ b/x-pack/solutions/observability/plugins/synthetics/server/routes/certs/get_certificates.ts @@ -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) { diff --git a/x-pack/solutions/observability/plugins/synthetics/server/routes/common.test.ts b/x-pack/solutions/observability/plugins/synthetics/server/routes/common.test.ts index ea981697db2e..119d6fa347c8 100644 --- a/x-pack/solutions/observability/plugins/synthetics/server/routes/common.test.ts +++ b/x-pack/solutions/observability/plugins/synthetics/server/routes/common.test.ts @@ -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")' ); }); }); diff --git a/x-pack/solutions/observability/plugins/synthetics/server/routes/common.ts b/x-pack/solutions/observability/plugins/synthetics/server/routes/common.ts index d5c9cb1ade6d..4c40c3e5f2ce 100644 --- a/x-pack/solutions/observability/plugins/synthetics/server/routes/common.ts +++ b/x-pack/solutions/observability/plugins/synthetics/server/routes/common.ts @@ -6,7 +6,6 @@ */ import { schema, Type, TypeOf } from '@kbn/config-schema'; -import { SavedObjectsFindResponse } from '@kbn/core/server'; import { isEmpty } from 'lodash'; import { escapeQuotes } from '@kbn/es-query'; import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common'; @@ -14,9 +13,8 @@ import { useLogicalAndFields } from '../../common/constants'; import { RouteContext } from './types'; import { MonitorSortFieldSchema } from '../../common/runtime_types/monitor_management/sort_field'; import { getAllLocations } from '../synthetics_service/get_all_locations'; -import { EncryptedSyntheticsMonitorAttributes } from '../../common/runtime_types'; import { PrivateLocation, ServiceLocation } from '../../common/runtime_types'; -import { monitorAttributes } from '../../common/types/saved_objects'; +import { syntheticsMonitorAttributes } from '../../common/types/saved_objects'; const StringOrArraySchema = schema.maybe( schema.oneOf([schema.string(), schema.arrayOf(schema.string())]) @@ -75,36 +73,6 @@ export const SEARCH_FIELDS = [ 'project_id.text', ]; -export const getMonitors = async ( - context: RouteContext, - { fields }: { fields?: string[] } = {} -): Promise> => { - const { - perPage = 50, - page, - sortField, - sortOrder, - query, - searchAfter, - showFromAllSpaces, - } = context.request.query; - - const { filtersStr } = await getMonitorFilters(context); - - return context.monitorConfigRepository.find({ - perPage, - page, - sortField: parseMappingKey(sortField), - sortOrder, - searchFields: SEARCH_FIELDS, - search: query, - filter: filtersStr, - searchAfter, - fields, - ...(showFromAllSpaces && { namespaces: ['*'] }), - }); -}; - interface Filters { filter?: string; tags?: string | string[]; @@ -117,7 +85,8 @@ interface Filters { } export const getMonitorFilters = async ( - context: RouteContext, OverviewStatusQuery> + context: RouteContext, OverviewStatusQuery>, + attr: string = syntheticsMonitorAttributes ) => { const { tags, @@ -141,7 +110,8 @@ export const getMonitorFilters = async ( monitorQueryIds, locations, }, - useLogicalAndFor + useLogicalAndFor, + attr ); }; @@ -156,7 +126,8 @@ export const parseArrayFilters = ( monitorQueryIds, locations, }: Filters, - useLogicalAndFor: MonitorsQuery['useLogicalAndFor'] = [] + useLogicalAndFor: MonitorsQuery['useLogicalAndFor'] = [], + attributes: string = syntheticsMonitorAttributes ) => { const filtersStr = [ filter, @@ -164,17 +135,19 @@ export const parseArrayFilters = ( field: 'tags', values: tags, operator: useLogicalAndFor.includes('tags') ? 'AND' : 'OR', + attributes, }), - getSavedObjectKqlFilter({ field: 'project_id', values: projects }), - getSavedObjectKqlFilter({ field: 'type', values: monitorTypes }), + getSavedObjectKqlFilter({ field: 'project_id', values: projects, attributes }), + getSavedObjectKqlFilter({ field: 'type', values: monitorTypes, attributes }), getSavedObjectKqlFilter({ field: 'locations.id', values: locations, operator: useLogicalAndFor.includes('locations') ? 'AND' : 'OR', + attributes, }), - getSavedObjectKqlFilter({ field: 'schedule.number', values: schedules }), - getSavedObjectKqlFilter({ field: 'id', values: monitorQueryIds }), - getSavedObjectKqlFilter({ field: 'config_id', values: configIds }), + getSavedObjectKqlFilter({ field: 'schedule.number', values: schedules, attributes }), + getSavedObjectKqlFilter({ field: 'id', values: monitorQueryIds, attributes }), + getSavedObjectKqlFilter({ field: 'config_id', values: configIds, attributes }), ] .filter((f) => !!f) .join(' AND '); @@ -187,11 +160,13 @@ export const getSavedObjectKqlFilter = ({ values, operator = 'OR', searchAtRoot = false, + attributes = syntheticsMonitorAttributes, }: { field: string; values?: string | string[]; operator?: string; searchAtRoot?: boolean; + attributes?: string; }) => { if (values === 'All' || (Array.isArray(values) && values?.includes('All'))) { return undefined; @@ -204,7 +179,7 @@ export const getSavedObjectKqlFilter = ({ if (searchAtRoot) { fieldKey = `${field}`; } else { - fieldKey = `${monitorAttributes}.${field}`; + fieldKey = `${attributes}.${field}`; } if (Array.isArray(values)) { diff --git a/x-pack/solutions/observability/plugins/synthetics/server/routes/default_alerts/default_alert_service.ts b/x-pack/solutions/observability/plugins/synthetics/server/routes/default_alerts/default_alert_service.ts index e26c4d8eefa5..e25208888a88 100644 --- a/x-pack/solutions/observability/plugins/synthetics/server/routes/default_alerts/default_alert_service.ts +++ b/x-pack/solutions/observability/plugins/synthetics/server/routes/default_alerts/default_alert_service.ts @@ -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 { diff --git a/x-pack/solutions/observability/plugins/synthetics/server/routes/default_alerts/update_default_alert.ts b/x-pack/solutions/observability/plugins/synthetics/server/routes/default_alerts/update_default_alert.ts index 3b2371ad5c2c..e0ee9bd471f2 100644 --- a/x-pack/solutions/observability/plugins/synthetics/server/routes/default_alerts/update_default_alert.ts +++ b/x-pack/solutions/observability/plugins/synthetics/server/routes/default_alerts/update_default_alert.ts @@ -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 => { + handler: async ({ context, server, savedObjectsClient }): Promise => { 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); diff --git a/x-pack/solutions/observability/plugins/synthetics/server/routes/filters/filters.ts b/x-pack/solutions/observability/plugins/synthetics/server/routes/filters/filters.ts index f0c43545ba13..aab30d16226d 100644 --- a/x-pack/solutions/observability/plugins/synthetics/server/routes/filters/filters.ts +++ b/x-pack/solutions/observability/plugins/synthetics/server/routes/filters/filters.ts @@ -6,7 +6,11 @@ */ import { schema } from '@kbn/config-schema'; import { SyntheticsRestApiRouteFactory } from '../types'; -import { monitorAttributes, syntheticsMonitorType } from '../../../common/types/saved_objects'; +import { + legacySyntheticsMonitorTypeSingle, + syntheticsMonitorAttributes, + syntheticsMonitorSavedObjectType, +} from '../../../common/types/saved_objects'; import { ConfigKey, MonitorFiltersResult } from '../../../common/runtime_types'; import { SYNTHETICS_API_URLS } from '../../../common/constants'; @@ -44,7 +48,7 @@ export const getSyntheticsFilters: SyntheticsRestApiRouteFactory => { 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 ({ 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 => { - 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) { diff --git a/x-pack/solutions/observability/plugins/synthetics/server/routes/monitor_cruds/add_monitor/add_monitor_api.test.ts b/x-pack/solutions/observability/plugins/synthetics/server/routes/monitor_cruds/add_monitor/add_monitor_api.test.ts index 4f707c51042c..5367a3b2d2d9 100644 --- a/x-pack/solutions/observability/plugins/synthetics/server/routes/monitor_cruds/add_monitor/add_monitor_api.test.ts +++ b/x-pack/solutions/observability/plugins/synthetics/server/routes/monitor_cruds/add_monitor/add_monitor_api.test.ts @@ -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.'); + }); + }); }); diff --git a/x-pack/solutions/observability/plugins/synthetics/server/routes/monitor_cruds/add_monitor/add_monitor_api.ts b/x-pack/solutions/observability/plugins/synthetics/server/routes/monitor_cruds/add_monitor/add_monitor_api.ts index aff104d69a20..5c908eb980eb 100644 --- a/x-pack/solutions/observability/plugins/synthetics/server/routes/monitor_cruds/add_monitor/add_monitor_api.ts +++ b/x-pack/solutions/observability/plugins/synthetics/server/routes/monitor_cruds/add_monitor/add_monitor_api.ts @@ -9,11 +9,15 @@ import { v4 as uuidV4 } from 'uuid'; import { SavedObject } from '@kbn/core-saved-objects-common/src/server_types'; import { isValidNamespace } from '@kbn/fleet-plugin/common'; import { i18n } from '@kbn/i18n'; +import { + legacySyntheticsMonitorTypeSingle, + syntheticsMonitorAttributes, + syntheticsMonitorSavedObjectType, +} from '../../../../common/types/saved_objects'; import { DeleteMonitorAPI } from '../services/delete_monitor_api'; import { parseMonitorLocations } from './utils'; import { MonitorValidationError } from '../monitor_validation'; import { getSavedObjectKqlFilter } from '../../common'; -import { monitorAttributes } from '../../../../common/types/saved_objects'; import { PrivateLocationAttributes } from '../../../runtime_types/private_locations'; import { ConfigKey } from '../../../../common/constants/monitor_management'; import { @@ -57,9 +61,11 @@ export class AddEditMonitorAPI { async syncNewMonitor({ id, normalizedMonitor, + savedObjectType, }: { id?: string; normalizedMonitor: SyntheticsMonitor; + savedObjectType?: string; }) { const { server, syntheticsMonitorClient, spaceId } = this.routeContext; const newMonitorId = id ?? uuidV4(); @@ -74,6 +80,8 @@ export class AddEditMonitorAPI { const newMonitorPromise = this.routeContext.monitorConfigRepository.create({ normalizedMonitor: monitorWithNamespace, id: newMonitorId, + spaceId, + savedObjectType, }); const syncErrorsPromise = syntheticsMonitorClient.addMonitors( @@ -205,7 +213,9 @@ export class AddEditMonitorAPI { const kqlFilter = getSavedObjectKqlFilter({ field: 'name.keyword', values: name }); const { total } = await monitorConfigRepository.find({ perPage: 0, - filter: id ? `${kqlFilter} and not (${monitorAttributes}.config_id: ${id})` : kqlFilter, + filter: id + ? `${kqlFilter} and not (${syntheticsMonitorAttributes}.config_id: ${id})` + : kqlFilter, }); if (total > 0) { @@ -217,7 +227,12 @@ export class AddEditMonitorAPI { } initDefaultAlerts(name: string) { - const { server, savedObjectsClient, context } = this.routeContext; + const { server, savedObjectsClient, context, request } = this.routeContext; + const { gettingStarted } = request.query; + if (!gettingStarted) { + return; + } + try { // we do this async, so we don't block the user, error handling will be done on the UI via separate api const defaultAlertService = new DefaultAlertService(context, server, savedObjectsClient); @@ -305,7 +320,10 @@ export class AddEditMonitorAPI { try { const encryptedMonitor = await monitorConfigRepository.get(newMonitorId); if (encryptedMonitor) { - await monitorConfigRepository.delete(newMonitorId); + await monitorConfigRepository.bulkDelete([ + { id: newMonitorId, type: syntheticsMonitorSavedObjectType }, + { id: newMonitorId, type: legacySyntheticsMonitorTypeSingle }, + ]); const deleteMonitorAPI = new DeleteMonitorAPI(this.routeContext); await deleteMonitorAPI.execute({ diff --git a/x-pack/solutions/observability/plugins/synthetics/server/routes/monitor_cruds/bulk_cruds/add_monitor_bulk.ts b/x-pack/solutions/observability/plugins/synthetics/server/routes/monitor_cruds/bulk_cruds/add_monitor_bulk.ts index f77dc54330e0..91f08cb44226 100644 --- a/x-pack/solutions/observability/plugins/synthetics/server/routes/monitor_cruds/bulk_cruds/add_monitor_bulk.ts +++ b/x-pack/solutions/observability/plugins/synthetics/server/routes/monitor_cruds/bulk_cruds/add_monitor_bulk.ts @@ -39,7 +39,8 @@ export const syncNewMonitorBulk = async ({ privateLocations: SyntheticsPrivateLocations; spaceId: string; }) => { - const { server, syntheticsMonitorClient, monitorConfigRepository } = routeContext; + const { server, syntheticsMonitorClient, monitorConfigRepository, request } = routeContext; + const { query } = request; let newMonitors: CreatedMonitors | null = null; const monitorsToCreate = normalizedMonitors.map((monitor) => { @@ -59,6 +60,7 @@ export const syncNewMonitorBulk = async ({ const [createdMonitors, [policiesResult, syncErrors]] = await Promise.all([ monitorConfigRepository.createBulk({ monitors: monitorsToCreate, + savedObjectType: query.savedObjectType, }), syntheticsMonitorClient.addMonitors(monitorsToCreate, privateLocations, spaceId), ]); diff --git a/x-pack/solutions/observability/plugins/synthetics/server/routes/monitor_cruds/bulk_cruds/edit_monitor_bulk.ts b/x-pack/solutions/observability/plugins/synthetics/server/routes/monitor_cruds/bulk_cruds/edit_monitor_bulk.ts index 61f609b1966c..7cedb1d56f4a 100644 --- a/x-pack/solutions/observability/plugins/synthetics/server/routes/monitor_cruds/bulk_cruds/edit_monitor_bulk.ts +++ b/x-pack/solutions/observability/plugins/synthetics/server/routes/monitor_cruds/bulk_cruds/edit_monitor_bulk.ts @@ -81,6 +81,7 @@ export const syncEditedMonitorBulk = async ({ [ConfigKey.MONITOR_QUERY_ID]: monitorWithRevision[ConfigKey.CUSTOM_HEARTBEAT_ID] || decryptedPreviousMonitor.id, } as unknown as MonitorFields, + soType: decryptedPreviousMonitor.type, })); const [editedMonitorSavedObjects, editSyncResponse] = await Promise.all([ monitorConfigRepository.bulkUpdate({ @@ -140,6 +141,7 @@ export const rollbackCompletely = async ({ monitors: monitorsToUpdate.map(({ decryptedPreviousMonitor }) => ({ id: decryptedPreviousMonitor.id, attributes: decryptedPreviousMonitor.attributes as unknown as MonitorFields, + soType: decryptedPreviousMonitor.type, })), }); } catch (error) { @@ -187,6 +189,7 @@ export const rollbackFailedUpdates = async ({ .map(({ decryptedPreviousMonitor }) => ({ id: decryptedPreviousMonitor.id, attributes: decryptedPreviousMonitor.attributes as unknown as MonitorFields, + soType: decryptedPreviousMonitor.type, })); if (monitorsToRevert.length > 0) { diff --git a/x-pack/solutions/observability/plugins/synthetics/server/routes/monitor_cruds/edit_monitor.test.ts b/x-pack/solutions/observability/plugins/synthetics/server/routes/monitor_cruds/edit_monitor.test.ts index 4e43f6d903da..ce497560a14a 100644 --- a/x-pack/solutions/observability/plugins/synthetics/server/routes/monitor_cruds/edit_monitor.test.ts +++ b/x-pack/solutions/observability/plugins/synthetics/server/routes/monitor_cruds/edit_monitor.test.ts @@ -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; - 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, - routeContext: { - syntheticsMonitorClient, - server: serverMock, - request: {} as unknown as KibanaRequest, - savedObjectsClient: - serverMock.authSavedObjectsClient as unknown as SavedObjectsClientContract, - } as any, + routeContext, spaceId: 'test-space', }); diff --git a/x-pack/solutions/observability/plugins/synthetics/server/routes/monitor_cruds/edit_monitor.ts b/x-pack/solutions/observability/plugins/synthetics/server/routes/monitor_cruds/edit_monitor.ts index 6b77d2acc101..616c14c53393 100644 --- a/x-pack/solutions/observability/plugins/synthetics/server/routes/monitor_cruds/edit_monitor.ts +++ b/x-pack/solutions/observability/plugins/synthetics/server/routes/monitor_cruds/edit_monitor.ts @@ -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 => { - 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(syntheticsMonitorType, configId, attributes); + await savedObjectsClient.update( + 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( - syntheticsMonitorType, + const editedSOPromise = monitorConfigRepository.update( decryptedPreviousMonitor.id, - formattedMonitor + formattedMonitor, + decryptedPreviousMonitor ); const allPrivateLocations = await getPrivateLocations(savedObjectsClient); diff --git a/x-pack/solutions/observability/plugins/synthetics/server/routes/monitor_cruds/get_monitor.ts b/x-pack/solutions/observability/plugins/synthetics/server/routes/monitor_cruds/get_monitor.ts index 434a42cec8b9..9e0b035efcf2 100644 --- a/x-pack/solutions/observability/plugins/synthetics/server/routes/monitor_cruds/get_monitor.ts +++ b/x-pack/solutions/observability/plugins/synthetics/server/routes/monitor_cruds/get_monitor.ts @@ -7,7 +7,6 @@ import { schema } from '@kbn/config-schema'; import { SavedObjectsErrorHelpers } from '@kbn/core/server'; import { SyntheticsRestApiRouteFactory } from '../types'; -import { syntheticsMonitorType } from '../../../common/types/saved_objects'; import { isStatusEnabled } from '../../../common/runtime_types/monitor_management/alert_config'; import { ConfigKey, EncryptedSyntheticsMonitorAttributes } from '../../../common/runtime_types'; import { SYNTHETICS_API_URLS } from '../../../common/constants'; @@ -35,8 +34,7 @@ export const getSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () => ({ handler: async ({ request, response, - server: { encryptedSavedObjects, coreStart }, - savedObjectsClient, + server: { coreStart }, spaceId, monitorConfigRepository, }): Promise => { @@ -54,17 +52,20 @@ export const getSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () => ({ if (Boolean(canSave)) { // only user with write permissions can decrypt the monitor const monitor = await monitorConfigRepository.getDecrypted(monitorId, spaceId); - return { ...mapSavedObjectToMonitor({ monitor, internal }), spaceId }; + return { + ...mapSavedObjectToMonitor({ monitor: monitor.normalizedMonitor, internal }), + spaceId, + spaces: monitor.decryptedMonitor.namespaces, + }; } else { + const monObj = await monitorConfigRepository.get(monitorId); return { ...mapSavedObjectToMonitor({ - monitor: await savedObjectsClient.get( - syntheticsMonitorType, - monitorId - ), + monitor: monObj, internal, }), spaceId, + spaces: monObj.namespaces, }; } } catch (getErr) { diff --git a/x-pack/solutions/observability/plugins/synthetics/server/routes/monitor_cruds/get_monitors_list.ts b/x-pack/solutions/observability/plugins/synthetics/server/routes/monitor_cruds/get_monitors_list.ts index 30751ab1c540..119a6604aeab 100644 --- a/x-pack/solutions/observability/plugins/synthetics/server/routes/monitor_cruds/get_monitors_list.ts +++ b/x-pack/solutions/observability/plugins/synthetics/server/routes/monitor_cruds/get_monitors_list.ts @@ -4,10 +4,18 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import { EncryptedSyntheticsMonitorAttributes } from '../../../common/runtime_types'; import { mapSavedObjectToMonitor } from './formatters/saved_object_to_monitor'; import { SyntheticsRestApiRouteFactory } from '../types'; import { SYNTHETICS_API_URLS } from '../../../common/constants'; -import { getMonitors, isMonitorsQueryFiltered, QuerySchema } from '../common'; +import { + getMonitorFilters, + isMonitorsQueryFiltered, + MonitorsQuery, + parseMappingKey, + QuerySchema, + SEARCH_FIELDS, +} from '../common'; export const getAllSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () => ({ method: 'GET', @@ -28,9 +36,22 @@ export const getAllSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () => }); } }; + const queryParams = routeContext.request.query as MonitorsQuery; + + const { filtersStr } = await getMonitorFilters(routeContext); const [queryResultSavedObjects, totalCount] = await Promise.all([ - getMonitors(routeContext), + monitorConfigRepository.find({ + perPage: queryParams.perPage ?? 50, + page: queryParams.page ?? 1, + sortField: parseMappingKey(queryParams.sortField), + sortOrder: queryParams.sortOrder, + searchFields: SEARCH_FIELDS, + search: queryParams.query, + filter: filtersStr, + searchAfter: queryParams.searchAfter, + ...(queryParams.showFromAllSpaces && { namespaces: ['*'] }), + }), totalCountQuery(), ]); @@ -46,8 +67,9 @@ export const getAllSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () => internal: request.query?.internal, }); return { - spaceId: monitor.namespaces?.[0], ...mon, + spaceId: monitor.namespaces?.[0], + spaces: monitor.namespaces ?? [], }; }), absoluteTotal, diff --git a/x-pack/solutions/observability/plugins/synthetics/server/routes/monitor_cruds/inspect_monitor.ts b/x-pack/solutions/observability/plugins/synthetics/server/routes/monitor_cruds/inspect_monitor.ts index 6d900bb29835..a819c6ecc0ff 100644 --- a/x-pack/solutions/observability/plugins/synthetics/server/routes/monitor_cruds/inspect_monitor.ts +++ b/x-pack/solutions/observability/plugins/synthetics/server/routes/monitor_cruds/inspect_monitor.ts @@ -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; diff --git a/x-pack/solutions/observability/plugins/synthetics/server/routes/monitor_cruds/monitor_validation.test.ts b/x-pack/solutions/observability/plugins/synthetics/server/routes/monitor_cruds/monitor_validation.test.ts index a6eacdfa6590..1909f3d628b6 100644 --- a/x-pack/solutions/observability/plugins/synthetics/server/routes/monitor_cruds/monitor_validation.test.ts +++ b/x-pack/solutions/observability/plugins/synthetics/server/routes/monitor_cruds/monitor_validation.test.ts @@ -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), } 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), } 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), } 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), } 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), } 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, diff --git a/x-pack/solutions/observability/plugins/synthetics/server/routes/monitor_cruds/monitor_validation.ts b/x-pack/solutions/observability/plugins/synthetics/server/routes/monitor_cruds/monitor_validation.ts index 832b054ff04b..a18bd81561c7 100644 --- a/x-pack/solutions/observability/plugins/synthetics/server/routes/monitor_cruds/monitor_validation.ts +++ b/x-pack/solutions/observability/plugins/synthetics/server/routes/monitor_cruds/monitor_validation.ts @@ -9,7 +9,7 @@ import { i18n } from '@kbn/i18n'; import { isLeft } from 'fp-ts/Either'; import { formatErrors } from '@kbn/securitysolution-io-ts-utils'; -import { omit } from 'lodash'; +import { omit, isEmpty } from 'lodash'; import { schema } from '@kbn/config-schema'; import { AlertConfigSchema } from '../../../common/runtime_types/monitor_management/alert_config_schema'; import { CreateMonitorPayLoad } from './add_monitor/add_monitor_api'; @@ -69,9 +69,11 @@ export class MonitorValidationError extends Error { /** * Validates monitor fields with respect to the relevant Codec identified by object's 'type' property. * @param monitorFields {MonitorFields} The mixed type representing the possible monitor payload from UI. + * @param spaceId */ -export function validateMonitor(monitorFields: MonitorFields): ValidationResult { - const { [ConfigKey.MONITOR_TYPE]: monitorType } = monitorFields; +export function validateMonitor(monitorFields: MonitorFields, spaceId: string): ValidationResult { + const { [ConfigKey.MONITOR_TYPE]: monitorType, [ConfigKey.KIBANA_SPACES]: kSpaces } = + monitorFields; if (monitorType !== MonitorTypeEnum.BROWSER && !monitorFields.name) { monitorFields.name = monitorFields.urls || monitorFields.hosts; @@ -159,6 +161,21 @@ export function validateMonitor(monitorFields: MonitorFields): ValidationResult } } + if (spaceId && !isEmpty(kSpaces)) { + // we throw error if kSpaces is not empty and spaceId is not present + if (kSpaces && !kSpaces.includes(spaceId) && !kSpaces.includes('*')) { + return { + valid: false, + reason: i18n.translate('xpack.synthetics.createMonitor.validation.invalidSpace', { + defaultMessage: + 'Invalid space ID provided in monitor configuration. It should always include the current space ID.', + }), + details: '', + payload: monitorFields, + }; + } + } + return { valid: true, reason: '', @@ -230,6 +247,10 @@ export const normalizeAPIConfig = (monitor: CreateMonitorPayLoad) => { let unsupportedKeys = Object.keys(rawConfig).filter((key) => !supportedKeys.includes(key)); const result = omit(rawConfig, unsupportedKeys); + let kSpaces = rawConfig[ConfigKey.KIBANA_SPACES] as string[]; + if (kSpaces?.includes('*')) { + kSpaces = ['*']; + } const formattedConfig = { ...result, @@ -237,6 +258,7 @@ export const normalizeAPIConfig = (monitor: CreateMonitorPayLoad) => { private_locations: _privateLocations, retest_on_failure: _retestOnFailure, custom_heartbeat_id: _customHeartbeatId, + ...(kSpaces ? { [ConfigKey.KIBANA_SPACES]: kSpaces } : {}), } as CreateMonitorPayLoad; const requestBodyCheck = formattedConfig[ConfigKey.REQUEST_BODY_CHECK]; diff --git a/x-pack/solutions/observability/plugins/synthetics/server/routes/monitor_cruds/project_monitor/add_monitor_project.ts b/x-pack/solutions/observability/plugins/synthetics/server/routes/monitor_cruds/project_monitor/add_monitor_project.ts index 6a4ef96704d3..2afb4aa6ef1f 100644 --- a/x-pack/solutions/observability/plugins/synthetics/server/routes/monitor_cruds/project_monitor/add_monitor_project.ts +++ b/x-pack/solutions/observability/plugins/synthetics/server/routes/monitor_cruds/project_monitor/add_monitor_project.ts @@ -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(), }), diff --git a/x-pack/solutions/observability/plugins/synthetics/server/routes/monitor_cruds/project_monitor/delete_monitor_project.ts b/x-pack/solutions/observability/plugins/synthetics/server/routes/monitor_cruds/project_monitor/delete_monitor_project.ts index c1a310f631c4..48a7fe694922 100644 --- a/x-pack/solutions/observability/plugins/synthetics/server/routes/monitor_cruds/project_monitor/delete_monitor_project.ts +++ b/x-pack/solutions/observability/plugins/synthetics/server/routes/monitor_cruds/project_monitor/delete_monitor_project.ts @@ -6,12 +6,12 @@ */ import { schema } from '@kbn/config-schema'; import { i18n } from '@kbn/i18n'; +import { syntheticsMonitorAttributes } from '../../../../common/types/saved_objects'; import { DeleteMonitorAPI } from '../services/delete_monitor_api'; import { SyntheticsRestApiRouteFactory } from '../../types'; -import { monitorAttributes } from '../../../../common/types/saved_objects'; -import { ConfigKey } from '../../../../common/runtime_types'; +import { ConfigKey, EncryptedSyntheticsMonitorAttributes } from '../../../../common/runtime_types'; import { SYNTHETICS_API_URLS } from '../../../../common/constants'; -import { getMonitors, getSavedObjectKqlFilter } from '../../common'; +import { getSavedObjectKqlFilter } from '../../common'; import { validateSpaceId } from '../services/validate_space_id'; export const deleteSyntheticsMonitorProjectRoute: SyntheticsRestApiRouteFactory = () => ({ @@ -26,7 +26,7 @@ export const deleteSyntheticsMonitorProjectRoute: SyntheticsRestApiRouteFactory }), }, handler: async (routeContext): Promise => { - const { request, response } = routeContext; + const { request, response, monitorConfigRepository } = routeContext; const { projectName } = request.params; const { monitors: monitorsToDelete } = request.body; const decodedProjectName = decodeURI(projectName); @@ -40,23 +40,19 @@ export const deleteSyntheticsMonitorProjectRoute: SyntheticsRestApiRouteFactory await validateSpaceId(routeContext); - const deleteFilter = `${monitorAttributes}.${ + const deleteFilter = `${syntheticsMonitorAttributes}.${ ConfigKey.PROJECT_ID }: "${decodedProjectName}" AND ${getSavedObjectKqlFilter({ field: 'journey_id', values: monitorsToDelete.map((id: string) => `${id}`), })}`; - const { saved_objects: monitors } = await getMonitors( - { - ...routeContext, - request: { - ...request, - query: { ...request.query, filter: deleteFilter, perPage: 500 }, - }, - }, - { fields: [] } - ); + const { saved_objects: monitors } = + await monitorConfigRepository.find({ + perPage: 500, + filter: deleteFilter, + fields: [], + }); const deleteMonitorAPI = new DeleteMonitorAPI(routeContext); diff --git a/x-pack/solutions/observability/plugins/synthetics/server/routes/monitor_cruds/project_monitor/get_monitor_project.ts b/x-pack/solutions/observability/plugins/synthetics/server/routes/monitor_cruds/project_monitor/get_monitor_project.ts index 9d6621cf508a..305e67333b12 100644 --- a/x-pack/solutions/observability/plugins/synthetics/server/routes/monitor_cruds/project_monitor/get_monitor_project.ts +++ b/x-pack/solutions/observability/plugins/synthetics/server/routes/monitor_cruds/project_monitor/get_monitor_project.ts @@ -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({ + 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] || '', diff --git a/x-pack/solutions/observability/plugins/synthetics/server/routes/monitor_cruds/services/delete_monitor_api.ts b/x-pack/solutions/observability/plugins/synthetics/server/routes/monitor_cruds/services/delete_monitor_api.ts index 37fdc9a6f8df..985aa9bb4eaa 100644 --- a/x-pack/solutions/observability/plugins/synthetics/server/routes/monitor_cruds/services/delete_monitor_api.ts +++ b/x-pack/solutions/observability/plugins/synthetics/server/routes/monitor_cruds/services/delete_monitor_api.ts @@ -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( - 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( - syntheticsMonitorType, + syntheticsMonitorSavedObjectType, monitorId ); } @@ -146,7 +136,7 @@ export class DeleteMonitorAPI { ); const deletePromise = this.routeContext.monitorConfigRepository.bulkDelete( - monitors.map((monitor) => monitor.id) + monitors.map((monitor) => ({ id: monitor.id, type: monitor.type })) ); const [errors, result] = await Promise.all([deleteSyncPromise, deletePromise]); diff --git a/x-pack/solutions/observability/plugins/synthetics/server/routes/overview_status/overview_status_service.test.ts b/x-pack/solutions/observability/plugins/synthetics/server/routes/overview_status/overview_status_service.test.ts index 6d73c3da1f35..a8465e73f105 100644 --- a/x-pack/solutions/observability/plugins/synthetics/server/routes/overview_status/overview_status_service.test.ts +++ b/x-pack/solutions/observability/plugins/synthetics/server/routes/overview_status/overview_status_service.test.ts @@ -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", diff --git a/x-pack/solutions/observability/plugins/synthetics/server/routes/overview_status/overview_status_service.ts b/x-pack/solutions/observability/plugins/synthetics/server/routes/overview_status/overview_status_service.ts index 8e723b60b5f5..935ac00c4e44 100644 --- a/x-pack/solutions/observability/plugins/synthetics/server/routes/overview_status/overview_status_service.ts +++ b/x-pack/solutions/observability/plugins/synthetics/server/routes/overview_status/overview_status_service.ts @@ -10,9 +10,10 @@ import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types'; import { SavedObjectsFindResult } from '@kbn/core-saved-objects-api-server'; import { isEmpty } from 'lodash'; import { withApmSpan } from '@kbn/apm-data-access-plugin/server/utils/with_apm_span'; +import { ALL_SPACES_ID } from '@kbn/security-plugin/common/constants'; import { asMutableArray } from '../../../common/utils/as_mutable_array'; import { getMonitorFilters, OverviewStatusQuery } from '../common'; -import { processMonitors } from '../../saved_objects/synthetics_monitor/get_all_monitors'; +import { processMonitors } from '../../saved_objects/synthetics_monitor/process_monitors'; import { ConfigKey } from '../../../common/constants/monitor_management'; import { RouteContext } from '../types'; import { @@ -115,7 +116,7 @@ export class OverviewStatusService { ]; }; const filters: QueryDslQueryContainer[] = [ - ...(showFromAllSpaces ? [] : [{ term: { 'meta.space_id': spaceId } }]), + ...(showFromAllSpaces ? [] : [{ terms: { 'meta.space_id': [spaceId, ALL_SPACES_ID] } }]), ...getTermFilter('monitor.type', monitorTypes), ...getTermFilter('tags', tags), ...getTermFilter('monitor.project.id', projects), @@ -370,7 +371,7 @@ export class OverviewStatusService { projectId: monitor.attributes[ConfigKey.PROJECT_ID], isStatusAlertEnabled: isStatusEnabled(monitor.attributes[ConfigKey.ALERT_CONFIG]), updated_at: monitor.updated_at, - spaceId: monitor.namespaces?.[0], + spaces: monitor.namespaces, urls: monitor.attributes[ConfigKey.URLS], maintenanceWindows: monitor.attributes[ConfigKey.MAINTENANCE_WINDOWS]?.map((mw) => mw), }; diff --git a/x-pack/solutions/observability/plugins/synthetics/server/routes/settings/dynamic_settings.ts b/x-pack/solutions/observability/plugins/synthetics/server/routes/settings/dynamic_settings.ts index e9b9bb6da931..21b158b8e656 100644 --- a/x-pack/solutions/observability/plugins/synthetics/server/routes/settings/dynamic_settings.ts +++ b/x-pack/solutions/observability/plugins/synthetics/server/routes/settings/dynamic_settings.ts @@ -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 => { 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); diff --git a/x-pack/solutions/observability/plugins/synthetics/server/routes/settings/private_locations/delete_private_location.ts b/x-pack/solutions/observability/plugins/synthetics/server/routes/settings/private_locations/delete_private_location.ts index d01b255dd2b3..dd70cf105b0c 100644 --- a/x-pack/solutions/observability/plugins/synthetics/server/routes/settings/private_locations/delete_private_location.ts +++ b/x-pack/solutions/observability/plugins/synthetics/server/routes/settings/private_locations/delete_private_location.ts @@ -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 { - 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 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).`, }, }); } diff --git a/x-pack/solutions/observability/plugins/synthetics/server/routes/settings/private_locations/get_location_monitors.ts b/x-pack/solutions/observability/plugins/synthetics/server/routes/settings/private_locations/get_location_monitors.ts index 6701946c8a6d..1c7e7ff298d4 100644 --- a/x-pack/solutions/observability/plugins/synthetics/server/routes/settings/private_locations/get_location_monitors.ts +++ b/x-pack/solutions/observability/plugins/synthetics/server/routes/settings/private_locations/get_location_monitors.ts @@ -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 = () => 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 = {}; + + 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({ - type: syntheticsMonitorType, - perPage: 0, - aggs, - filter: locationFilter, - namespaces: [ALL_SPACES_ID], - }); - - return ( - locationMonitors.aggregations?.locations.buckets.map(({ key: id, doc_count: count }) => ({ - id, - count, - })) ?? [] - ); -}; diff --git a/x-pack/solutions/observability/plugins/synthetics/server/routes/settings/private_locations/helpers.test.ts b/x-pack/solutions/observability/plugins/synthetics/server/routes/settings/private_locations/helpers.test.ts index 5a271747c8d3..9f6e36da8f92 100644 --- a/x-pack/solutions/observability/plugins/synthetics/server/routes/settings/private_locations/helpers.test.ts +++ b/x-pack/solutions/observability/plugins/synthetics/server/routes/settings/private_locations/helpers.test.ts @@ -151,6 +151,7 @@ describe('updatePrivateLocationMonitors', () => { schedule: { number: '10', unit: 'm' }, namespace: FIRST_SPACE_ID, }, + namespaces: [FIRST_SPACE_ID], }, { id: SECOND_MONITOR_ID, @@ -166,6 +167,7 @@ describe('updatePrivateLocationMonitors', () => { schedule: { number: '5', unit: 'm' }, namespace: SECOND_SPACE_ID, }, + namespaces: [SECOND_SPACE_ID], }, ]; diff --git a/x-pack/solutions/observability/plugins/synthetics/server/routes/settings/private_locations/helpers.ts b/x-pack/solutions/observability/plugins/synthetics/server/routes/settings/private_locations/helpers.ts index 85579112411d..afb43fbd8aa5 100644 --- a/x-pack/solutions/observability/plugins/synthetics/server/routes/settings/private_locations/helpers.ts +++ b/x-pack/solutions/observability/plugins/synthetics/server/routes/settings/private_locations/helpers.ts @@ -101,21 +101,21 @@ export const updatePrivateLocationMonitors = async ({ monitorWithRevision, }; - const namespace = m.attributes.namespace; + const spaceId = m.namespaces?.[0] || 'default'; // Default to 'default' if no namespace is found return { ...acc, - [namespace]: [...(acc[namespace] || []), monitorToUpdate], + [spaceId]: [...(acc[spaceId] || []), monitorToUpdate], }; }, {} ); - const promises = Object.keys(updatedMonitorsPerSpace).map((namespace) => [ + const promises = Object.keys(updatedMonitorsPerSpace).map((spaceId) => [ syncEditedMonitorBulk({ - monitorsToUpdate: updatedMonitorsPerSpace[namespace], + monitorsToUpdate: updatedMonitorsPerSpace[spaceId], privateLocations: allPrivateLocations, routeContext, - spaceId: namespace, + spaceId, }), ]); diff --git a/x-pack/solutions/observability/plugins/synthetics/server/routes/suggestions/route.ts b/x-pack/solutions/observability/plugins/synthetics/server/routes/suggestions/route.ts deleted file mode 100644 index 8a1c486c39fb..000000000000 --- a/x-pack/solutions/observability/plugins/synthetics/server/routes/suggestions/route.ts +++ /dev/null @@ -1,166 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import { SyntheticsRestApiRouteFactory } from '../types'; -import { monitorAttributes, syntheticsMonitorType } from '../../../common/types/saved_objects'; -import { - ConfigKey, - MonitorFiltersResult, - EncryptedSyntheticsMonitorAttributes, -} from '../../../common/runtime_types'; -import { SYNTHETICS_API_URLS } from '../../../common/constants'; -import { QuerySchema, getMonitorFilters, SEARCH_FIELDS } from '../common'; -import { getAllLocations } from '../../synthetics_service/get_all_locations'; - -type Buckets = Array<{ - key: string; - doc_count: number; -}>; - -interface AggsResponse { - locationsAggs: { - buckets: Buckets; - }; - tagsAggs: { - buckets: Buckets; - }; - projectsAggs: { - buckets: Buckets; - }; - monitorTypesAggs: { - buckets: Buckets; - }; - monitorIdsAggs: { - buckets: Array<{ - key: string; - doc_count: number; - name: { - hits: { - hits: Array<{ - _source: { - [syntheticsMonitorType]: { - [ConfigKey.NAME]: string; - }; - }; - }>; - }; - }; - }>; - }; -} - -export const getSyntheticsSuggestionsRoute: SyntheticsRestApiRouteFactory< - MonitorFiltersResult -> = () => ({ - method: 'GET', - path: SYNTHETICS_API_URLS.SUGGESTIONS, - validate: { - query: QuerySchema, - }, - handler: async (route): Promise => { - const { monitorConfigRepository } = route; - const { query } = route.request.query; - - const { filtersStr } = await getMonitorFilters(route); - const { allLocations = [] } = await getAllLocations(route); - const data = await monitorConfigRepository.find({ - 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, - }, - }, - }, - }, -}; diff --git a/x-pack/solutions/observability/plugins/synthetics/server/routes/suggestions/suggestions_route.ts b/x-pack/solutions/observability/plugins/synthetics/server/routes/suggestions/suggestions_route.ts new file mode 100644 index 000000000000..ea6aa80c9261 --- /dev/null +++ b/x-pack/solutions/observability/plugins/synthetics/server/routes/suggestions/suggestions_route.ts @@ -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(); + 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(); + 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 => { + 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({ + 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({ + 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, + })) ?? [], + }; + }, +}); diff --git a/x-pack/solutions/observability/plugins/synthetics/server/routes/synthetics_service/run_once_monitor.ts b/x-pack/solutions/observability/plugins/synthetics/server/routes/synthetics_service/run_once_monitor.ts index 549e2c95ecbd..191838ee4ce4 100644 --- a/x-pack/solutions/observability/plugins/synthetics/server/routes/synthetics_service/run_once_monitor.ts +++ b/x-pack/solutions/observability/plugins/synthetics/server/routes/synthetics_service/run_once_monitor.ts @@ -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 => { 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) { diff --git a/x-pack/solutions/observability/plugins/synthetics/server/routes/synthetics_service/test_now_monitor.ts b/x-pack/solutions/observability/plugins/synthetics/server/routes/synthetics_service/test_now_monitor.ts index edd827df7f3f..a5939fcbe6df 100644 --- a/x-pack/solutions/observability/plugins/synthetics/server/routes/synthetics_service/test_now_monitor.ts +++ b/x-pack/solutions/observability/plugins/synthetics/server/routes/synthetics_service/test_now_monitor.ts @@ -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> => { - 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, diff --git a/x-pack/solutions/observability/plugins/synthetics/server/saved_objects/index.ts b/x-pack/solutions/observability/plugins/synthetics/server/saved_objects/index.ts index ee1cfbbc55ac..e486f6812049 100644 --- a/x-pack/solutions/observability/plugins/synthetics/server/saved_objects/index.ts +++ b/x-pack/solutions/observability/plugins/synthetics/server/saved_objects/index.ts @@ -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'; diff --git a/x-pack/solutions/observability/plugins/synthetics/server/saved_objects/migrations/monitors/8.6.0.ts b/x-pack/solutions/observability/plugins/synthetics/server/saved_objects/migrations/monitors/8.6.0.ts index 2cf529bdda57..d4443414e91c 100644 --- a/x-pack/solutions/observability/plugins/synthetics/server/saved_objects/migrations/monitors/8.6.0.ts +++ b/x-pack/solutions/observability/plugins/synthetics/server/saved_objects/migrations/monitors/8.6.0.ts @@ -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, diff --git a/x-pack/solutions/observability/plugins/synthetics/server/saved_objects/migrations/monitors/8.8.0.test.ts b/x-pack/solutions/observability/plugins/synthetics/server/saved_objects/migrations/monitors/8.8.0.test.ts index 8ebfba92dd10..cf9e75712451 100644 --- a/x-pack/solutions/observability/plugins/synthetics/server/saved_objects/migrations/monitors/8.8.0.test.ts +++ b/x-pack/solutions/observability/plugins/synthetics/server/saved_objects/migrations/monitors/8.8.0.test.ts @@ -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', diff --git a/x-pack/solutions/observability/plugins/synthetics/server/saved_objects/migrations/monitors/8.8.0.ts b/x-pack/solutions/observability/plugins/synthetics/server/saved_objects/migrations/monitors/8.8.0.ts index f165cee329f3..8dc1743b81b2 100644 --- a/x-pack/solutions/observability/plugins/synthetics/server/saved_objects/migrations/monitors/8.8.0.ts +++ b/x-pack/solutions/observability/plugins/synthetics/server/saved_objects/migrations/monitors/8.8.0.ts @@ -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( diff --git a/x-pack/solutions/observability/plugins/synthetics/server/saved_objects/migrations/monitors/8.9.0.ts b/x-pack/solutions/observability/plugins/synthetics/server/saved_objects/migrations/monitors/8.9.0.ts index d0ee4859b8a8..d3d273e35ad6 100644 --- a/x-pack/solutions/observability/plugins/synthetics/server/saved_objects/migrations/monitors/8.9.0.ts +++ b/x-pack/solutions/observability/plugins/synthetics/server/saved_objects/migrations/monitors/8.9.0.ts @@ -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, }); }; diff --git a/x-pack/solutions/observability/plugins/synthetics/server/saved_objects/saved_objects.ts b/x-pack/solutions/observability/plugins/synthetics/server/saved_objects/saved_objects.ts index d59ecb507166..c7d6bc5fe72d 100644 --- a/x-pack/solutions/observability/plugins/synthetics/server/saved_objects/saved_objects.ts +++ b/x-pack/solutions/observability/plugins/synthetics/server/saved_objects/saved_objects.ts @@ -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 => { - try { - const obj = await client.get( - 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( - syntheticsSettingsObjectType, - settings, - { - id: syntheticsSettingsObjectId, - overwrite: true, - } - ); - - return settingsObject.attributes; - }, -}; - -const getUptimeDynamicSettings = async (client: SavedObjectsClientContract) => { - try { - const obj = await client.get( - 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); }; diff --git a/x-pack/solutions/observability/plugins/synthetics/server/saved_objects/synthetics_monitor.ts b/x-pack/solutions/observability/plugins/synthetics/server/saved_objects/synthetics_monitor.ts index 3af697840d61..1fec1c76430e 100644 --- a/x-pack/solutions/observability/plugins/synthetics/server/saved_objects/synthetics_monitor.ts +++ b/x-pack/solutions/observability/plugins/synthetics/server/saved_objects/synthetics_monitor.ts @@ -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( - syntheticsMonitorType, - monitorId, - { - namespace: spaceId, - } - ); -}; diff --git a/x-pack/solutions/observability/plugins/synthetics/server/saved_objects/synthetics_monitor/index.ts b/x-pack/solutions/observability/plugins/synthetics/server/saved_objects/synthetics_monitor/index.ts new file mode 100644 index 000000000000..6d5dc7976f11 --- /dev/null +++ b/x-pack/solutions/observability/plugins/synthetics/server/saved_objects/synthetics_monitor/index.ts @@ -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'; diff --git a/x-pack/solutions/observability/plugins/synthetics/server/saved_objects/synthetics_monitor/legacy_synthetics_monitor.ts b/x-pack/solutions/observability/plugins/synthetics/server/saved_objects/synthetics_monitor/legacy_synthetics_monitor.ts new file mode 100644 index 000000000000..06ee09e56b89 --- /dev/null +++ b/x-pack/solutions/observability/plugins/synthetics/server/saved_objects/synthetics_monitor/legacy_synthetics_monitor.ts @@ -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, + ]), +}; diff --git a/x-pack/solutions/observability/plugins/synthetics/server/saved_objects/synthetics_monitor/monitor_mappings.ts b/x-pack/solutions/observability/plugins/synthetics/server/saved_objects/synthetics_monitor/monitor_mappings.ts new file mode 100644 index 000000000000..d0ee2d56a66d --- /dev/null +++ b/x-pack/solutions/observability/plugins/synthetics/server/saved_objects/synthetics_monitor/monitor_mappings.ts @@ -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', + }, + }, +}; diff --git a/x-pack/solutions/observability/plugins/synthetics/server/saved_objects/synthetics_monitor/get_all_monitors.test.ts b/x-pack/solutions/observability/plugins/synthetics/server/saved_objects/synthetics_monitor/process_monitors.test.ts similarity index 99% rename from x-pack/solutions/observability/plugins/synthetics/server/saved_objects/synthetics_monitor/get_all_monitors.test.ts rename to x-pack/solutions/observability/plugins/synthetics/server/saved_objects/synthetics_monitor/process_monitors.test.ts index 156f939c33ff..95970ca0692c 100644 --- a/x-pack/solutions/observability/plugins/synthetics/server/saved_objects/synthetics_monitor/get_all_monitors.test.ts +++ b/x-pack/solutions/observability/plugins/synthetics/server/saved_objects/synthetics_monitor/process_monitors.test.ts @@ -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', () => { diff --git a/x-pack/solutions/observability/plugins/synthetics/server/saved_objects/synthetics_monitor/get_all_monitors.ts b/x-pack/solutions/observability/plugins/synthetics/server/saved_objects/synthetics_monitor/process_monitors.ts similarity index 100% rename from x-pack/solutions/observability/plugins/synthetics/server/saved_objects/synthetics_monitor/get_all_monitors.ts rename to x-pack/solutions/observability/plugins/synthetics/server/saved_objects/synthetics_monitor/process_monitors.ts diff --git a/x-pack/solutions/observability/plugins/synthetics/server/saved_objects/synthetics_monitor/synthetics_monitor_config.ts b/x-pack/solutions/observability/plugins/synthetics/server/saved_objects/synthetics_monitor/synthetics_monitor_config.ts new file mode 100644 index 000000000000..243d71285089 --- /dev/null +++ b/x-pack/solutions/observability/plugins/synthetics/server/saved_objects/synthetics_monitor/synthetics_monitor_config.ts @@ -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, +}; diff --git a/x-pack/solutions/observability/plugins/synthetics/server/saved_objects/synthetics_param.ts b/x-pack/solutions/observability/plugins/synthetics/server/saved_objects/synthetics_param.ts index be9422cf7d3a..975cf0ffb8c5 100644 --- a/x-pack/solutions/observability/plugins/synthetics/server/saved_objects/synthetics_param.ts +++ b/x-pack/solutions/observability/plugins/synthetics/server/saved_objects/synthetics_param.ts @@ -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']), diff --git a/x-pack/solutions/observability/plugins/synthetics/server/saved_objects/synthetics_settings.ts b/x-pack/solutions/observability/plugins/synthetics/server/saved_objects/synthetics_settings.ts index 63deacf534c9..3c4fcafae847 100644 --- a/x-pack/solutions/observability/plugins/synthetics/server/saved_objects/synthetics_settings.ts +++ b/x-pack/solutions/observability/plugins/synthetics/server/saved_objects/synthetics_settings.ts @@ -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 => { + try { + const obj = await client.get( + 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( + syntheticsSettingsObjectType, + settings, + { + id: syntheticsSettingsObjectId, + overwrite: true, + } + ); + + return settingsObject.attributes; +}; + +const getUptimeDynamicSettings = async (client: SavedObjectsClientContract) => { + try { + const obj = await client.get( + uptimeSettingsObjectType, + uptimeSettingsObjectId + ); + return obj?.attributes ?? DYNAMIC_SETTINGS_DEFAULT_ATTRIBUTES; + } catch (getErr) { + return DYNAMIC_SETTINGS_DEFAULT_ATTRIBUTES; + } +}; diff --git a/x-pack/solutions/observability/plugins/synthetics/server/services/monitor_config_repository.test.ts b/x-pack/solutions/observability/plugins/synthetics/server/services/monitor_config_repository.test.ts index a612f4b0cb37..91de6be225bc 100644 --- a/x-pack/solutions/observability/plugins/synthetics/server/services/monitor_config_repository.test.ts +++ b/x-pack/solutions/observability/plugins/synthetics/server/services/monitor_config_repository.test.ts @@ -7,12 +7,20 @@ import { savedObjectsClientMock } from '@kbn/core-saved-objects-api-server-mocks'; import { MonitorConfigRepository } from './monitor_config_repository'; -import { syntheticsMonitorType } from '../../common/types/saved_objects'; import { ConfigKey, SyntheticsMonitor } from '../../common/runtime_types'; import * as utils from '../synthetics_service/utils'; import { encryptedSavedObjectsMock } from '@kbn/encrypted-saved-objects-plugin/server/mocks'; import { EncryptedSavedObjectsClient } from '@kbn/encrypted-saved-objects-plugin/server'; -import { SavedObjectsClientContract } from '@kbn/core-saved-objects-api-server'; +import { + SavedObjectsClientContract, + type SavedObjectsFindOptions, +} from '@kbn/core-saved-objects-api-server'; +import { + legacyMonitorAttributes, + legacySyntheticsMonitorTypeSingle, + syntheticsMonitorAttributes, + syntheticsMonitorSavedObjectType, +} from '../../common/types/saved_objects'; // Mock the utils functions jest.mock('../synthetics_service/utils', () => ({ @@ -47,25 +55,33 @@ describe('MonitorConfigRepository', () => { const mockMonitor = { id, attributes: { name: 'Test Monitor' }, - type: syntheticsMonitorType, + type: syntheticsMonitorSavedObjectType, references: [], }; - soClient.get.mockResolvedValue(mockMonitor); + soClient.bulkGet.mockResolvedValue({ saved_objects: [mockMonitor] }); const result = await repository.get(id); - expect(soClient.get).toHaveBeenCalledWith(syntheticsMonitorType, id); + expect(soClient.bulkGet).toHaveBeenCalledWith([ + { type: 'synthetics-monitor-multi-space', id }, + { + type: 'synthetics-monitor', + id, + }, + ]); expect(result).toBe(mockMonitor); }); it('should propagate errors', async () => { const id = 'test-id'; - const error = new Error('Not found'); + const error = new Error(`Failed to get monitor with id ${id}: Not found`); - soClient.get.mockRejectedValue(error); + soClient.bulkGet.mockRejectedValue(error); - await expect(repository.get(id)).rejects.toThrow(error); + await expect(repository.get(id)).rejects.toThrow( + /Failed to get monitor with id test-id: Not found/ + ); }); }); @@ -76,7 +92,7 @@ describe('MonitorConfigRepository', () => { const mockDecryptedMonitor = { id, attributes: { name: 'Test Monitor', secrets: 'decrypted' }, - type: syntheticsMonitorType, + type: syntheticsMonitorSavedObjectType, references: [], }; @@ -85,18 +101,20 @@ describe('MonitorConfigRepository', () => { ); (utils.normalizeSecrets as jest.Mock).mockReturnValue({ ...mockDecryptedMonitor, - normalizedSecrets: true, }); const result = await repository.getDecrypted(id, spaceId); expect(encryptedSavedObjectsClient.getDecryptedAsInternalUser).toHaveBeenCalledWith( - syntheticsMonitorType, + syntheticsMonitorSavedObjectType, id, { namespace: spaceId } ); expect(utils.normalizeSecrets).toHaveBeenCalledWith(mockDecryptedMonitor); - expect(result).toEqual({ ...mockDecryptedMonitor, normalizedSecrets: true }); + expect(result).toEqual({ + decryptedMonitor: mockDecryptedMonitor, + normalizedMonitor: mockDecryptedMonitor, + }); }); }); @@ -111,7 +129,7 @@ describe('MonitorConfigRepository', () => { const mockCreatedMonitor = { id, attributes: { name: 'Test Monitor' }, - type: syntheticsMonitorType, + type: syntheticsMonitorSavedObjectType, references: [], }; soClient.create.mockResolvedValue(mockCreatedMonitor); @@ -119,6 +137,7 @@ describe('MonitorConfigRepository', () => { const result = await repository.create({ id, normalizedMonitor, + spaceId: 'default', }); expect(utils.formatSecrets).toHaveBeenCalledWith({ @@ -126,18 +145,20 @@ describe('MonitorConfigRepository', () => { [ConfigKey.MONITOR_QUERY_ID]: 'custom-id', [ConfigKey.CONFIG_ID]: id, revision: 1, + spaces: ['default'], }); expect(soClient.create).toHaveBeenCalledWith( - syntheticsMonitorType, + syntheticsMonitorSavedObjectType, { ...normalizedMonitor, [ConfigKey.MONITOR_QUERY_ID]: 'custom-id', [ConfigKey.CONFIG_ID]: id, revision: 1, formattedSecrets: true, + spaces: ['default'], }, - { id, overwrite: true } + { id, overwrite: true, initialNamespaces: ['default'] } ); expect(result).toBe(mockCreatedMonitor); @@ -151,33 +172,36 @@ describe('MonitorConfigRepository', () => { const mockCreatedMonitor = { id: 'generated-id', attributes: { name: 'Test Monitor' }, - type: syntheticsMonitorType, + type: syntheticsMonitorSavedObjectType, references: [], }; soClient.create.mockResolvedValue(mockCreatedMonitor); const result = await repository.create({ - id: '', + id: 'test', normalizedMonitor, + spaceId: 'default', }); expect(utils.formatSecrets).toHaveBeenCalledWith({ ...normalizedMonitor, - [ConfigKey.MONITOR_QUERY_ID]: '', - [ConfigKey.CONFIG_ID]: '', + [ConfigKey.MONITOR_QUERY_ID]: 'test', + [ConfigKey.CONFIG_ID]: 'test', revision: 1, + spaces: ['default'], }); expect(soClient.create).toHaveBeenCalledWith( - syntheticsMonitorType, + syntheticsMonitorSavedObjectType, { ...normalizedMonitor, - [ConfigKey.MONITOR_QUERY_ID]: '', - [ConfigKey.CONFIG_ID]: '', + [ConfigKey.MONITOR_QUERY_ID]: 'test', + [ConfigKey.CONFIG_ID]: 'test', revision: 1, formattedSecrets: true, + spaces: ['default'], }, - undefined + { id: 'test', overwrite: true, initialNamespaces: ['default'] } ); expect(result).toBe(mockCreatedMonitor); @@ -207,13 +231,13 @@ describe('MonitorConfigRepository', () => { { id: 'test-id-1', attributes: { name: 'Test Monitor 1' }, - type: syntheticsMonitorType, + type: syntheticsMonitorSavedObjectType, references: [], }, { id: 'test-id-2', attributes: { name: 'Test Monitor 2' }, - type: syntheticsMonitorType, + type: syntheticsMonitorSavedObjectType, references: [], }, ], @@ -226,7 +250,7 @@ describe('MonitorConfigRepository', () => { expect(soClient.bulkCreate).toHaveBeenCalledWith([ { id: 'test-id-1', - type: syntheticsMonitorType, + type: syntheticsMonitorSavedObjectType, attributes: { name: 'Test Monitor 1', [ConfigKey.CUSTOM_HEARTBEAT_ID]: 'custom-id-1', @@ -238,7 +262,7 @@ describe('MonitorConfigRepository', () => { }, { id: 'test-id-2', - type: syntheticsMonitorType, + type: syntheticsMonitorSavedObjectType, attributes: { name: 'Test Monitor 2', [ConfigKey.MONITOR_QUERY_ID]: 'test-id-2', @@ -261,12 +285,14 @@ describe('MonitorConfigRepository', () => { attributes: { name: 'Updated Monitor 1', }, + soType: 'synthetics-monitor-multi-space', }, { id: 'test-id-2', attributes: { name: 'Updated Monitor 2', }, + soType: 'synthetics-monitor', }, ] as any; @@ -275,13 +301,13 @@ describe('MonitorConfigRepository', () => { { id: 'test-id-1', attributes: { name: 'Updated Monitor 1' }, - type: syntheticsMonitorType, + type: syntheticsMonitorSavedObjectType, references: [], }, { id: 'test-id-2', attributes: { name: 'Updated Monitor 2' }, - type: syntheticsMonitorType, + type: syntheticsMonitorSavedObjectType, references: [], }, ], @@ -293,12 +319,12 @@ describe('MonitorConfigRepository', () => { expect(soClient.bulkUpdate).toHaveBeenCalledWith([ { - type: syntheticsMonitorType, + type: syntheticsMonitorSavedObjectType, id: 'test-id-1', attributes: { name: 'Updated Monitor 1' }, }, { - type: syntheticsMonitorType, + type: 'synthetics-monitor', id: 'test-id-2', attributes: { name: 'Updated Monitor 2' }, }, @@ -316,6 +342,7 @@ describe('MonitorConfigRepository', () => { perPage: 10, sortField: 'name', sortOrder: 'asc' as const, + filter: `${syntheticsMonitorAttributes}.enabled:true`, }; const mockFindResult = { @@ -323,13 +350,13 @@ describe('MonitorConfigRepository', () => { { id: 'test-id-1', attributes: { name: 'Test Monitor 1' }, - type: syntheticsMonitorType, + type: syntheticsMonitorSavedObjectType, references: [], }, { id: 'test-id-2', attributes: { name: 'Test Monitor 2' }, - type: syntheticsMonitorType, + type: syntheticsMonitorSavedObjectType, references: [], }, ], @@ -338,16 +365,31 @@ describe('MonitorConfigRepository', () => { page: 1, } as any; - soClient.find.mockResolvedValue(mockFindResult); + soClient.find.mockImplementation((opts: SavedObjectsFindOptions) => { + if (opts.type !== syntheticsMonitorSavedObjectType) { + return { + saved_objects: [], + total: 0, + per_page: 0, + page: 1, + }; + } + return mockFindResult; + }); const result = await repository.find(options); expect(soClient.find).toHaveBeenCalledWith({ - type: syntheticsMonitorType, + type: syntheticsMonitorSavedObjectType, ...options, }); - expect(result).toBe(mockFindResult); + expect(soClient.find).toHaveBeenLastCalledWith({ + type: legacySyntheticsMonitorTypeSingle, + ...{ ...options, filter: 'synthetics-monitor.attributes.enabled:true' }, + }); + + expect(result).toStrictEqual(mockFindResult); }); it('should use default perPage if not provided', async () => { @@ -367,7 +409,7 @@ describe('MonitorConfigRepository', () => { await repository.find(options); expect(soClient.find).toHaveBeenCalledWith({ - type: syntheticsMonitorType, + type: syntheticsMonitorSavedObjectType, search: 'test', perPage: 5000, }); @@ -383,13 +425,13 @@ describe('MonitorConfigRepository', () => { { id: 'test-id-1', attributes: { name: 'Test Monitor 1', secrets: 'decrypted' }, - type: syntheticsMonitorType, + type: syntheticsMonitorSavedObjectType, references: [], }, { id: 'test-id-2', attributes: { name: 'Test Monitor 2', secrets: 'decrypted' }, - type: syntheticsMonitorType, + type: syntheticsMonitorSavedObjectType, references: [], }, ]; @@ -411,14 +453,14 @@ describe('MonitorConfigRepository', () => { encryptedSavedObjectsClient.createPointInTimeFinderDecryptedAsInternalUser ).toHaveBeenCalledWith({ filter, - type: syntheticsMonitorType, + type: syntheticsMonitorSavedObjectType, perPage: 500, namespaces: [spaceId], }); expect(pointInTimeFinderMock.find).toHaveBeenCalled(); expect(pointInTimeFinderMock.close).toHaveBeenCalled(); - expect(result).toEqual(mockDecryptedMonitors); + expect(result).toEqual([...mockDecryptedMonitors, ...mockDecryptedMonitors]); }); it('should handle finder.close errors', async () => { @@ -428,7 +470,7 @@ describe('MonitorConfigRepository', () => { { id: 'test-id-1', attributes: { name: 'Test Monitor 1', secrets: 'decrypted' }, - type: syntheticsMonitorType, + type: syntheticsMonitorSavedObjectType, references: [], }, ]; @@ -447,38 +489,32 @@ describe('MonitorConfigRepository', () => { const result = await repository.findDecryptedMonitors({ spaceId }); expect(pointInTimeFinderMock.close).toHaveBeenCalled(); - expect(result).toEqual(mockDecryptedMonitors); + expect(result).toEqual([...mockDecryptedMonitors, ...mockDecryptedMonitors]); // Should not throw an error when close fails }); }); - describe('delete', () => { - it('should delete a monitor by id', async () => { - const id = 'test-id'; - const mockDeleteResult = { success: true }; - - soClient.delete.mockResolvedValue(mockDeleteResult); - - const result = await repository.delete(id); - - expect(soClient.delete).toHaveBeenCalledWith(syntheticsMonitorType, id); - expect(result).toBe(mockDeleteResult); - }); - }); - describe('bulkDelete', () => { it('should delete multiple monitors by ids', async () => { - const ids = ['test-id-1', 'test-id-2']; + const ids = [ + { id: 'test-id-1', type: syntheticsMonitorSavedObjectType }, + { id: 'test-id-2', type: syntheticsMonitorSavedObjectType }, + ]; const mockBulkDeleteResult = { success: true } as any; soClient.bulkDelete.mockResolvedValue(mockBulkDeleteResult); const result = await repository.bulkDelete(ids); - expect(soClient.bulkDelete).toHaveBeenCalledWith([ - { type: syntheticsMonitorType, id: 'test-id-1' }, - { type: syntheticsMonitorType, id: 'test-id-2' }, - ]); + expect(soClient.bulkDelete).toHaveBeenCalledWith( + [ + { type: syntheticsMonitorSavedObjectType, id: 'test-id-1' }, + { type: syntheticsMonitorSavedObjectType, id: 'test-id-2' }, + ], + { + force: true, + } + ); expect(result).toBe(mockBulkDeleteResult); }); @@ -513,7 +549,7 @@ describe('MonitorConfigRepository', () => { const result = await repository.getAll(options); expect(soClient.createPointInTimeFinder).toHaveBeenCalledWith({ - type: syntheticsMonitorType, + type: syntheticsMonitorSavedObjectType, perPage: 5000, search: 'test', fields: ['name'], @@ -524,7 +560,7 @@ describe('MonitorConfigRepository', () => { namespaces: ['*'], }); - expect(result).toEqual(mockMonitors); + expect(result).toEqual([...mockMonitors, ...mockMonitors]); }); it('should not include namespaces if showFromAllSpaces is false', async () => { @@ -547,7 +583,7 @@ describe('MonitorConfigRepository', () => { await repository.getAll(options); expect(soClient.createPointInTimeFinder).toHaveBeenCalledWith({ - type: syntheticsMonitorType, + type: syntheticsMonitorSavedObjectType, perPage: 5000, search: 'test', sortField: 'name.keyword', @@ -574,7 +610,7 @@ describe('MonitorConfigRepository', () => { await repository.getAll(options); expect(soClient.createPointInTimeFinder).toHaveBeenCalledWith({ - type: syntheticsMonitorType, + type: syntheticsMonitorSavedObjectType, perPage: 5000, search: 'test', sortField: 'name.keyword', @@ -599,8 +635,98 @@ describe('MonitorConfigRepository', () => { const result = await repository.getAll(options); expect(pointInTimeFinderMock.close).toHaveBeenCalled(); - expect(result).toEqual(mockMonitors); + expect(result).toEqual([...mockMonitors, ...mockMonitors]); // Should not throw an error when close fails }); }); + + // Mock logger to spy on its methods + const mockLogger = { + error: jest.fn(), + }; + + describe('handleLegacyOptions', () => { + // Clear mock history before each test + beforeEach(() => { + mockLogger.error.mockClear(); + }); + + // Restore any mocks after all tests are done + afterAll(() => { + jest.restoreAllMocks(); + }); + + test('should convert legacy attributes to new attributes for synthetics-monitor type', () => { + const options = { + search: 'my-monitor', + search_fields: [`${legacyMonitorAttributes}.name`, 'status'], + sortField: 'name', + filter: `${legacyMonitorAttributes}.enabled:true`, + }; + const type = syntheticsMonitorSavedObjectType; + + const expectedOptions = { + filter: 'synthetics-monitor-multi-space.attributes.enabled:true', + search: 'my-monitor', + search_fields: ['synthetics-monitor-multi-space.attributes.name', 'status'], + sortField: 'name', + }; + + const result = repository.handleLegacyOptions(options, type); + expect(result).toEqual(expectedOptions); + expect(mockLogger.error).not.toHaveBeenCalled(); + }); + + test('should convert new attributes back to legacy for the legacy monitor type', () => { + const options = { + search_fields: [`${syntheticsMonitorAttributes}.name`, 'status'], + sortField: `${syntheticsMonitorAttributes}.name`, + }; + const legacyType = legacySyntheticsMonitorTypeSingle; + + const expectedOptions = { + search_fields: ['synthetics-monitor.attributes.name', 'status'], + sortField: 'synthetics-monitor.attributes.name', + }; + + const result = repository.handleLegacyOptions(options, legacyType); + expect(result).toEqual(expectedOptions); + expect(mockLogger.error).not.toHaveBeenCalled(); + }); + + test('should return options unchanged if no target attributes are found for a matching type', () => { + const options = { + search: 'a-monitor', + search_fields: ['status'], + }; + const type = 'synthetics-monitor'; + + const result = repository.handleLegacyOptions(options, type); + expect(result).toEqual(options); + }); + + test('should handle an empty options object without errors', () => { + const options = {}; + const type = 'synthetics-monitor'; + + const result = repository.handleLegacyOptions(options, type); + expect(result).toEqual({}); + }); + + test('should handle options with null or undefined values', () => { + const options = { + search_fields: ['monitor.legacy.name', null], + sortField: undefined, + }; + const type = 'synthetics-monitor'; + + const expectedOptions = { + search_fields: ['monitor.legacy.name', null], + sortField: undefined, + }; + + const result = repository.handleLegacyOptions(options, type); + expect(result).toEqual(expectedOptions); + }); + }); }); diff --git a/x-pack/solutions/observability/plugins/synthetics/server/services/monitor_config_repository.ts b/x-pack/solutions/observability/plugins/synthetics/server/services/monitor_config_repository.ts index 487d58e95ef0..f31533c47e6c 100644 --- a/x-pack/solutions/observability/plugins/synthetics/server/services/monitor_config_repository.ts +++ b/x-pack/solutions/observability/plugins/synthetics/server/services/monitor_config_repository.ts @@ -8,13 +8,23 @@ import { SavedObject, SavedObjectsClientContract, + type SavedObjectsCreateOptions, SavedObjectsFindOptions, + type SavedObjectsFindResponse, SavedObjectsFindResult, } from '@kbn/core-saved-objects-api-server'; import { EncryptedSavedObjectsClient } from '@kbn/encrypted-saved-objects-plugin/server'; import { withApmSpan } from '@kbn/apm-data-access-plugin/server/utils/with_apm_span'; +import { isEmpty, isEqual } from 'lodash'; +import { Logger } from '@kbn/logging'; +import { + legacyMonitorAttributes, + legacySyntheticsMonitorTypeSingle, + syntheticsMonitorAttributes, + syntheticsMonitorSavedObjectType, + syntheticsMonitorSOTypes, +} from '../../common/types/saved_objects'; import { formatSecrets, normalizeSecrets } from '../synthetics_service/utils'; -import { syntheticsMonitorType } from '../../common/types/saved_objects'; import { ConfigKey, EncryptedSyntheticsMonitorAttributes, @@ -23,50 +33,117 @@ import { SyntheticsMonitorWithSecretsAttributes, } from '../../common/runtime_types'; +const getSuccessfulResult = ( + results: Array> +): PromiseFulfilledResult['value'] => { + for (const result of results) { + if (result.status === 'fulfilled') { + return result.value; + } + } + const firstError = results.find((r): r is PromiseRejectedResult => r.status === 'rejected'); + throw firstError?.reason || new Error('Unknown error'); +}; + export class MonitorConfigRepository { constructor( private soClient: SavedObjectsClientContract, - private encryptedSavedObjectsClient: EncryptedSavedObjectsClient + private encryptedSavedObjectsClient: EncryptedSavedObjectsClient, + private logger?: Logger // Replace with appropriate logger type ) {} async get(id: string) { - return await this.soClient.get(syntheticsMonitorType, id); + // we need to resolve both syntheticsMonitorSavedObjectType and legacySyntheticsMonitorTypeSingle + const results = await this.soClient.bulkGet([ + { type: syntheticsMonitorSavedObjectType, id }, + { type: legacySyntheticsMonitorTypeSingle, id }, + ]); + const resolved = results.saved_objects.find((obj) => obj?.attributes); + if (!resolved) { + throw new Error('Monitor not found'); + } + return resolved; } - async getDecrypted(id: string, spaceId: string): Promise> { - const decryptedMonitor = - await this.encryptedSavedObjectsClient.getDecryptedAsInternalUser( - syntheticsMonitorType, + async getDecrypted( + id: string, + spaceId: string + ): Promise<{ + normalizedMonitor: SavedObject; + decryptedMonitor: SavedObject; + }> { + const namespace = { namespace: spaceId }; + + // Helper to attempt decryption and catch 404 + const tryGetDecrypted = async (soType: string) => { + return await this.encryptedSavedObjectsClient.getDecryptedAsInternalUser( + soType, id, - { - namespace: spaceId, - } + namespace ); - return normalizeSecrets(decryptedMonitor); + }; + + const results = await Promise.allSettled([ + tryGetDecrypted(syntheticsMonitorSavedObjectType), + tryGetDecrypted(legacySyntheticsMonitorTypeSingle), + ]); + + const decryptedMonitor = getSuccessfulResult(results); + + return { + normalizedMonitor: normalizeSecrets(decryptedMonitor), + decryptedMonitor, + }; } - async create({ id, normalizedMonitor }: { id: string; normalizedMonitor: SyntheticsMonitor }) { + async create({ + id, + spaceId, + normalizedMonitor, + savedObjectType, + }: { + id: string; + normalizedMonitor: SyntheticsMonitor; + spaceId: string; + savedObjectType?: string; + }) { + let { spaces } = normalizedMonitor; + // Ensure spaceId is included in spaces + if (isEmpty(spaces)) { + spaces = [spaceId]; + } else if (!spaces?.includes(spaceId)) { + spaces = [...(spaces ?? []), spaceId]; + } + + const opts: SavedObjectsCreateOptions = { + id, + ...(id && { overwrite: true }), + ...(!isEmpty(spaces) && { initialNamespaces: spaces }), + }; + return await this.soClient.create( - syntheticsMonitorType, + savedObjectType ?? syntheticsMonitorSavedObjectType, formatSecrets({ ...normalizedMonitor, [ConfigKey.MONITOR_QUERY_ID]: normalizedMonitor[ConfigKey.CUSTOM_HEARTBEAT_ID] || id, [ConfigKey.CONFIG_ID]: id, revision: 1, + [ConfigKey.KIBANA_SPACES]: spaces, }), - id - ? { - id, - overwrite: true, - } - : undefined + opts ); } - async createBulk({ monitors }: { monitors: Array<{ id: string; monitor: MonitorFields }> }) { + async createBulk({ + monitors, + savedObjectType, + }: { + monitors: Array<{ id: string; monitor: MonitorFields }>; + savedObjectType?: string; + }) { const newMonitors = monitors.map(({ id, monitor }) => ({ id, - type: syntheticsMonitorType, + type: savedObjectType ?? syntheticsMonitorSavedObjectType, attributes: formatSecrets({ ...monitor, [ConfigKey.MONITOR_QUERY_ID]: monitor[ConfigKey.CUSTOM_HEARTBEAT_ID] || id, @@ -80,6 +157,27 @@ export class MonitorConfigRepository { return result.saved_objects; } + async update( + id: string, + data: SyntheticsMonitorWithSecretsAttributes, + decryptedPreviousMonitor: SavedObject + ) { + const soType = decryptedPreviousMonitor.type; + const prevSpaces = (decryptedPreviousMonitor.namespaces || []).sort(); + + const spaces = (data.spaces || []).sort(); + // If the spaces have changed, we need to delete the saved object and recreate it + if (isEqual(prevSpaces, spaces)) { + return this.soClient.update(soType, id, data); + } else { + await this.soClient.delete(soType, id, { force: true }); + return await this.soClient.create(syntheticsMonitorSavedObjectType, data, { + id, + ...(!isEmpty(spaces) && { initialNamespaces: spaces }), + }); + } + } + async bulkUpdate({ monitors, namespace, @@ -87,12 +185,13 @@ export class MonitorConfigRepository { monitors: Array<{ attributes: MonitorFields; id: string; + soType: string; }>; namespace?: string; }) { return this.soClient.bulkUpdate( - monitors.map(({ attributes, id }) => ({ - type: syntheticsMonitorType, + monitors.map(({ attributes, id, soType }) => ({ + type: soType, id, attributes, namespace, @@ -100,44 +199,70 @@ export class MonitorConfigRepository { ); } - find(options: Omit) { - return this.soClient.find({ - type: syntheticsMonitorType, - ...options, - perPage: options.perPage ?? 5000, + async find( + options: Omit, + types: string[] = syntheticsMonitorSOTypes, + soClient: SavedObjectsClientContract = this.soClient + ): Promise> { + const promises: Array>> = types.map((type) => { + const opts = { + type, + ...options, + perPage: options.perPage ?? 5000, + }; + return soClient.find(this.handleLegacyOptions(opts, type)); }); + const [result, legacyResult] = await Promise.all(promises); + return { + ...result, + total: result.total + legacyResult.total, + saved_objects: [...result.saved_objects, ...legacyResult.saved_objects], + }; } async findDecryptedMonitors({ spaceId, filter }: { spaceId: string; filter?: string }) { - const finder = - await this.encryptedSavedObjectsClient.createPointInTimeFinderDecryptedAsInternalUser( - { - filter, - type: syntheticsMonitorType, - perPage: 500, - namespaces: [spaceId], - } - ); + const getDecrypted = async (soType: string) => { + // Handle legacy filter if the type is legacy + const legacyFilter = + soType === legacySyntheticsMonitorTypeSingle ? this.handleLegacyFilter(filter) : filter; + const finder = + await this.encryptedSavedObjectsClient.createPointInTimeFinderDecryptedAsInternalUser( + { + filter: legacyFilter, + type: soType, + perPage: 500, + namespaces: [spaceId], + } + ); - const decryptedMonitors: Array> = - []; - for await (const result of finder.find()) { - decryptedMonitors.push(...result.saved_objects); - } + const decryptedMonitors: Array< + SavedObjectsFindResult + > = []; + for await (const result of finder.find()) { + decryptedMonitors.push(...result.saved_objects); + } - finder.close().catch(() => {}); + finder.close().catch(() => {}); - return decryptedMonitors; + return decryptedMonitors; + }; + + const [decryptedMonitors, legacyDecryptedMonitors] = await Promise.all([ + getDecrypted(syntheticsMonitorSavedObjectType), + getDecrypted(legacySyntheticsMonitorTypeSingle), + ]); + return [...decryptedMonitors, ...legacyDecryptedMonitors]; } - async delete(monitorId: string) { - return this.soClient.delete(syntheticsMonitorType, monitorId); - } - - async bulkDelete(monitorIds: string[]) { - return this.soClient.bulkDelete( - monitorIds.map((monitor) => ({ type: syntheticsMonitorType, id: monitor })) - ); + async bulkDelete( + monitors: Array<{ + id: string; + type: string; + }> + ) { + return this.soClient.bulkDelete(monitors, { + force: true, + }); } async getAll< @@ -155,7 +280,12 @@ export class MonitorConfigRepository { filter?: string; showFromAllSpaces?: boolean; } & Pick) { - return withApmSpan('get_all_monitors', async () => { + const getConfigs = async (syntheticsMonitorType: string) => { + const findFilter = + syntheticsMonitorType === legacySyntheticsMonitorTypeSingle + ? this.handleLegacyFilter(filter) + : filter; + const finder = this.soClient.createPointInTimeFinder({ type: syntheticsMonitorType, perPage: 5000, @@ -163,7 +293,7 @@ export class MonitorConfigRepository { sortField, sortOrder, fields, - filter, + filter: findFilter, searchFields, ...(showFromAllSpaces && { namespaces: ['*'] }), }); @@ -176,6 +306,41 @@ export class MonitorConfigRepository { finder.close().catch(() => {}); return hits; + }; + + return withApmSpan('get_all_monitors', async () => { + const [configs, legacyConfigs] = await Promise.all([ + getConfigs(syntheticsMonitorSavedObjectType), + getConfigs(legacySyntheticsMonitorTypeSingle), + ]); + return [...configs, ...legacyConfigs]; }); } + + handleLegacyFilter = (filter?: string): string | undefined => { + if (!filter) { + return filter; + } + // Replace syntheticsMonitorAttributes with legacyMonitorAttributes in the filter + return filter.replace(new RegExp(syntheticsMonitorAttributes, 'g'), legacyMonitorAttributes); + }; + + handleLegacyOptions(options: Omit, type: string) { + // convert the options to string and replace if the type is opposite of either of the synthetics monitor types + try { + const opts = JSON.stringify(options); + if (type === syntheticsMonitorSavedObjectType) { + return JSON.parse( + opts.replace(new RegExp(legacyMonitorAttributes, 'g'), syntheticsMonitorAttributes) + ); + } else if (type === legacySyntheticsMonitorTypeSingle) { + return JSON.parse( + opts.replace(new RegExp(syntheticsMonitorAttributes, 'g'), legacyMonitorAttributes) + ); + } + } catch (e) { + this.logger?.error(`Error parsing handleLegacyOptions: ${e}`); + return options; + } + } } diff --git a/x-pack/solutions/observability/plugins/synthetics/server/synthetics_service/formatters/private_formatters/common_formatters.ts b/x-pack/solutions/observability/plugins/synthetics/server/synthetics_service/formatters/private_formatters/common_formatters.ts index b6088daee7a0..3ab454fbc6b3 100644 --- a/x-pack/solutions/observability/plugins/synthetics/server/synthetics_service/formatters/private_formatters/common_formatters.ts +++ b/x-pack/solutions/observability/plugins/synthetics/server/synthetics_service/formatters/private_formatters/common_formatters.ts @@ -39,6 +39,7 @@ export const commonFormatters: CommonFormatMap = { [ConfigKey.PARAMS]: null, [ConfigKey.MAX_ATTEMPTS]: null, [ConfigKey.MAINTENANCE_WINDOWS]: null, + [ConfigKey.KIBANA_SPACES]: null, retest_on_failure: null, [ConfigKey.SCHEDULE]: (fields) => JSON.stringify( diff --git a/x-pack/solutions/observability/plugins/synthetics/server/synthetics_service/formatters/private_formatters/processors_formatter.ts b/x-pack/solutions/observability/plugins/synthetics/server/synthetics_service/formatters/private_formatters/processors_formatter.ts index cb9ffea41e6a..abd71acc6a8a 100644 --- a/x-pack/solutions/observability/plugins/synthetics/server/synthetics_service/formatters/private_formatters/processors_formatter.ts +++ b/x-pack/solutions/observability/plugins/synthetics/server/synthetics_service/formatters/private_formatters/processors_formatter.ts @@ -18,6 +18,8 @@ interface FieldProcessor { export const processorsFormatter = (config: MonitorFields & ProcessorFields) => { const labels = config[ConfigKey.LABELS] ?? {}; + const kSpaces = config[ConfigKey.KIBANA_SPACES] ?? []; + const spaces = Array.from(new Set([config.space_id, ...kSpaces])); const processors: FieldProcessor[] = [ { add_fields: { @@ -30,7 +32,7 @@ export const processorsFormatter = (config: MonitorFields & ProcessorFields) => 'monitor.project.name': config['monitor.project.name'], 'monitor.project.id': config['monitor.project.id'], meta: { - space_id: config.space_id, + space_id: spaces.length === 1 ? spaces[0] : spaces, }, ...(isEmpty(labels) ? {} : { labels }), }, diff --git a/x-pack/solutions/observability/plugins/synthetics/server/synthetics_service/formatters/public_formatters/common.ts b/x-pack/solutions/observability/plugins/synthetics/server/synthetics_service/formatters/public_formatters/common.ts index 2c767f79a65b..b673b0671812 100644 --- a/x-pack/solutions/observability/plugins/synthetics/server/synthetics_service/formatters/public_formatters/common.ts +++ b/x-pack/solutions/observability/plugins/synthetics/server/synthetics_service/formatters/public_formatters/common.ts @@ -48,6 +48,7 @@ export const commonFormatters: CommonFormatMap = { [ConfigKey.MONITOR_QUERY_ID]: null, retest_on_failure: null, [ConfigKey.MAX_ATTEMPTS]: maxAttemptsFormatter, + [ConfigKey.KIBANA_SPACES]: null, [ConfigKey.TIMEOUT]: secondsToCronFormatter, [ConfigKey.MONITOR_SOURCE_TYPE]: (fields) => fields[ConfigKey.MONITOR_SOURCE_TYPE] || SourceType.UI, diff --git a/x-pack/solutions/observability/plugins/synthetics/server/synthetics_service/formatters/public_formatters/format_configs.ts b/x-pack/solutions/observability/plugins/synthetics/server/synthetics_service/formatters/public_formatters/format_configs.ts index a8c60db03e85..90fd47fa5b25 100644 --- a/x-pack/solutions/observability/plugins/synthetics/server/synthetics_service/formatters/public_formatters/format_configs.ts +++ b/x-pack/solutions/observability/plugins/synthetics/server/synthetics_service/formatters/public_formatters/format_configs.ts @@ -113,7 +113,8 @@ export const formatHeartbeatRequest = ( const heartbeatIdT = heartbeatId ?? monitor[ConfigKey.MONITOR_QUERY_ID]; const paramsString = params ?? (monitor as BrowserFields)[ConfigKey.PARAMS]; - const { labels } = monitor; + const { labels, spaces } = monitor; + const monSpaces = spaces ? Array.from(new Set([...(spaces ?? []), spaceId])) : spaceId; return { ...monitor, @@ -125,7 +126,7 @@ export const formatHeartbeatRequest = ( run_once: runOnce, test_run_id: testRunId, meta: { - space_id: spaceId, + space_id: monSpaces, }, ...(isEmpty(labels) ? {} : { labels }), }, diff --git a/x-pack/solutions/observability/plugins/synthetics/server/synthetics_service/private_location/synthetics_private_location.test.ts b/x-pack/solutions/observability/plugins/synthetics/server/synthetics_service/private_location/synthetics_private_location.test.ts index 1d4ff0977f88..5ca514b57b89 100644 --- a/x-pack/solutions/observability/plugins/synthetics/server/synthetics_service/private_location/synthetics_private_location.test.ts +++ b/x-pack/solutions/observability/plugins/synthetics/server/synthetics_service/private_location/synthetics_private_location.test.ts @@ -116,7 +116,8 @@ describe('SyntheticsPrivateLocation', () => { await syntheticsPrivateLocation.createPackagePolicies( [{ config: testConfig, globalParams: {} }], [mockPrivateLocation], - 'test-space' + 'test-space', + [] ); } catch (e) { expect(e).toEqual(new Error(error)); diff --git a/x-pack/solutions/observability/plugins/synthetics/server/synthetics_service/private_location/synthetics_private_location.ts b/x-pack/solutions/observability/plugins/synthetics/server/synthetics_service/private_location/synthetics_private_location.ts index dc856d8d1a26..7baf8ae40ee5 100644 --- a/x-pack/solutions/observability/plugins/synthetics/server/synthetics_service/private_location/synthetics_private_location.ts +++ b/x-pack/solutions/observability/plugins/synthetics/server/synthetics_service/private_location/synthetics_private_location.ts @@ -139,6 +139,7 @@ export class SyntheticsPrivateLocation { configs: PrivateConfig[], privateLocations: SyntheticsPrivateLocations, spaceId: string, + maintenanceWindows: MaintenanceWindow[], testRunId?: string, runOnce?: boolean ) { @@ -168,7 +169,7 @@ export class SyntheticsPrivateLocation { newPolicyTemplate, spaceId, globalParams, - [], + maintenanceWindows, testRunId, runOnce ); diff --git a/x-pack/solutions/observability/plugins/synthetics/server/synthetics_service/project_monitor/project_monitor_formatter.test.ts b/x-pack/solutions/observability/plugins/synthetics/server/synthetics_service/project_monitor/project_monitor_formatter.test.ts index 3c8833a67d42..3d777437e643 100644 --- a/x-pack/solutions/observability/plugins/synthetics/server/synthetics_service/project_monitor/project_monitor_formatter.test.ts +++ b/x-pack/solutions/observability/plugins/synthetics/server/synthetics_service/project_monitor/project_monitor_formatter.test.ts @@ -492,14 +492,14 @@ const soData = [ ...payloadData[0], revision: 1, } as any), - type: 'synthetics-monitor', + type: 'synthetics-monitor-multi-space', }, { attributes: formatSecrets({ ...payloadData[1], revision: 1, } as any), - type: 'synthetics-monitor', + type: 'synthetics-monitor-multi-space', }, ]; diff --git a/x-pack/solutions/observability/plugins/synthetics/server/synthetics_service/project_monitor/project_monitor_formatter.ts b/x-pack/solutions/observability/plugins/synthetics/server/synthetics_service/project_monitor/project_monitor_formatter.ts index 7c1d4440fc7b..b54cfb9decd1 100644 --- a/x-pack/solutions/observability/plugins/synthetics/server/synthetics_service/project_monitor/project_monitor_formatter.ts +++ b/x-pack/solutions/observability/plugins/synthetics/server/synthetics_service/project_monitor/project_monitor_formatter.ts @@ -6,11 +6,11 @@ */ import { SavedObjectsUpdateResponse, SavedObjectsClientContract } from '@kbn/core/server'; import { i18n } from '@kbn/i18n'; +import { syntheticsMonitorSavedObjectType } from '../../../common/types/saved_objects'; import { getSavedObjectKqlFilter } from '../../routes/common'; import { InvalidLocationError } from './normalizers/common_fields'; import { SyntheticsServerSetup } from '../../types'; import { RouteContext } from '../../routes/types'; -import { syntheticsMonitorType } from '../../../common/types/saved_objects'; import { getAllLocations } from '../get_all_locations'; import { syncNewMonitorBulk } from '../../routes/monitor_cruds/bulk_cruds/add_monitor_bulk'; import { SyntheticsMonitorClient } from '../synthetics_monitor/synthetics_monitor_client'; @@ -97,7 +97,7 @@ export class ProjectMonitorFormatter { this.syntheticsMonitorClient = routeContext.syntheticsMonitorClient; this.monitors = monitors; this.server = routeContext.server; - this.projectFilter = `${syntheticsMonitorType}.attributes.${ConfigKey.PROJECT_ID}: "${this.projectId}"`; + this.projectFilter = `${syntheticsMonitorSavedObjectType}.attributes.${ConfigKey.PROJECT_ID}: "${this.projectId}"`; this.publicLocations = []; this.privateLocations = []; } @@ -222,7 +222,7 @@ export class ProjectMonitorFormatter { /* Validates that the normalized monitor is a valid monitor saved object type */ const { valid: isNormalizedMonitorValid, decodedMonitor } = this.validateMonitor({ - validationResult: validateMonitor(normalizedMonitor as MonitorFields), + validationResult: validateMonitor(normalizedMonitor as MonitorFields, this.spaceId), monitorId: monitor.id, }); diff --git a/x-pack/solutions/observability/plugins/synthetics/server/synthetics_service/synthetics_monitor/synthetics_monitor_client.ts b/x-pack/solutions/observability/plugins/synthetics/server/synthetics_service/synthetics_monitor/synthetics_monitor_client.ts index 3385a1ecb7f1..584a9524d35e 100644 --- a/x-pack/solutions/observability/plugins/synthetics/server/synthetics_service/synthetics_monitor/synthetics_monitor_client.ts +++ b/x-pack/solutions/observability/plugins/synthetics/server/synthetics_service/synthetics_monitor/synthetics_monitor_client.ts @@ -75,7 +75,8 @@ export class SyntheticsMonitorClient { const newPolicies = this.privateLocationAPI.createPackagePolicies( privateConfigs, allPrivateLocations, - spaceId + spaceId, + maintenanceWindows ); const syncErrors = this.syntheticsService.addConfigs(publicConfigs, maintenanceWindows); @@ -224,6 +225,7 @@ export class SyntheticsMonitorClient { privateConfig ? [privateConfig] : [], allPrivateLocations, spaceId, + [], monitor.testRunId, runOnce ); diff --git a/x-pack/solutions/observability/plugins/synthetics/server/synthetics_service/synthetics_service.ts b/x-pack/solutions/observability/plugins/synthetics/server/synthetics_service/synthetics_service.ts index fcda398d15f1..ea4209ee592b 100644 --- a/x-pack/solutions/observability/plugins/synthetics/server/synthetics_service/synthetics_service.ts +++ b/x-pack/solutions/observability/plugins/synthetics/server/synthetics_service/synthetics_service.ts @@ -24,7 +24,11 @@ import { isEmpty } from 'lodash'; import { MAINTENANCE_WINDOW_SAVED_OBJECT_TYPE } from '@kbn/alerting-plugin/common'; import { registerCleanUpTask } from './private_location/clean_up_task'; import { SyntheticsServerSetup } from '../types'; -import { syntheticsMonitorType, syntheticsParamType } from '../../common/types/saved_objects'; +import { + legacySyntheticsMonitorTypeSingle, + syntheticsMonitorSavedObjectType, + syntheticsParamType, +} from '../../common/types/saved_objects'; import { sendErrorTelemetryEvents } from '../routes/telemetry/monitor_upgrade_sender'; import { installSyntheticsIndexTemplates } from '../routes/synthetics_service/install_index_templates'; import { getAPIKeyForSyntheticsService } from './get_api_key'; @@ -299,7 +303,7 @@ export class SyntheticsService { return await encryptedClient.createPointInTimeFinderDecryptedAsInternalUser( { - type: syntheticsMonitorType, + type: [legacySyntheticsMonitorTypeSingle, syntheticsMonitorSavedObjectType], perPage: pageSize, namespaces: [ALL_SPACES_ID], } diff --git a/x-pack/solutions/observability/test/api_integration/apis/synthetics/add_monitor.ts b/x-pack/solutions/observability/test/api_integration/apis/synthetics/add_monitor.ts index 6be8d38a21e5..40368e7d350e 100644 --- a/x-pack/solutions/observability/test/api_integration/apis/synthetics/add_monitor.ts +++ b/x-pack/solutions/observability/test/api_integration/apis/synthetics/add_monitor.ts @@ -14,7 +14,7 @@ import { HTTPFields } from '@kbn/synthetics-plugin/common/runtime_types'; import { SYNTHETICS_API_URLS } from '@kbn/synthetics-plugin/common/constants'; import { ALL_SPACES_ID } from '@kbn/security-plugin/common/constants'; import { getServiceApiKeyPrivileges } from '@kbn/synthetics-plugin/server/synthetics_service/get_api_key'; -import { syntheticsMonitorType } from '@kbn/synthetics-plugin/common/types/saved_objects'; +import { legacySyntheticsMonitorTypeSingle } from '@kbn/synthetics-plugin/common/types/saved_objects'; import { removeMonitorEmptyValues, transformPublicKeys, @@ -224,7 +224,7 @@ export default function ({ getService }: FtrProviderContext) { .get(SYNTHETICS_API_URLS.SYNTHETICS_MONITORS) .auth(username, password) .query({ - filter: `${syntheticsMonitorType}.attributes.name: "${name}"`, + filter: `${legacySyntheticsMonitorTypeSingle}.attributes.name: "${name}"`, }) .set('kbn-xsrf', 'true') .expect(200); diff --git a/x-pack/solutions/observability/test/api_integration/apis/synthetics/add_monitor_private_location.ts b/x-pack/solutions/observability/test/api_integration/apis/synthetics/add_monitor_private_location.ts index c6b3e96c3b47..d1b8945695d4 100644 --- a/x-pack/solutions/observability/test/api_integration/apis/synthetics/add_monitor_private_location.ts +++ b/x-pack/solutions/observability/test/api_integration/apis/synthetics/add_monitor_private_location.ts @@ -93,6 +93,7 @@ export default function ({ getService }: FtrProviderContext) { ...monitor, [ConfigKey.NAMESPACE]: formatKibanaNamespace(SPACE_ID), url: apiResponse.body.url, + spaces: [SPACE_ID], }) ); monitorId = apiResponse.body.id; diff --git a/x-pack/solutions/observability/test/api_integration/apis/synthetics/add_monitor_project.ts b/x-pack/solutions/observability/test/api_integration/apis/synthetics/add_monitor_project.ts index ddfba5e6d632..e2c06ff452bd 100644 --- a/x-pack/solutions/observability/test/api_integration/apis/synthetics/add_monitor_project.ts +++ b/x-pack/solutions/observability/test/api_integration/apis/synthetics/add_monitor_project.ts @@ -9,7 +9,7 @@ import expect from '@kbn/expect'; import { ConfigKey, ProjectMonitorsRequest } from '@kbn/synthetics-plugin/common/runtime_types'; import { SYNTHETICS_API_URLS } from '@kbn/synthetics-plugin/common/constants'; import { formatKibanaNamespace } from '@kbn/synthetics-plugin/common/formatters'; -import { syntheticsMonitorType } from '@kbn/synthetics-plugin/common/types/saved_objects'; +import { syntheticsMonitorSavedObjectType } from '@kbn/synthetics-plugin/common/types/saved_objects'; import { FtrProviderContext } from '../../ftr_provider_context'; import { getFixtureJson } from './helper/get_fixture_json'; import { PrivateLocationTestService } from './services/private_location_test_service'; @@ -45,7 +45,7 @@ export default function ({ getService }: FtrProviderContext) { const response = await supertest .get(`/s/${space}${SYNTHETICS_API_URLS.SYNTHETICS_MONITORS}`) .query({ - filter: `${syntheticsMonitorType}.attributes.journey_id: "${journeyId}" AND ${syntheticsMonitorType}.attributes.project_id: "${projectId}"`, + filter: `${syntheticsMonitorSavedObjectType}.attributes.journey_id: "${journeyId}" AND ${syntheticsMonitorSavedObjectType}.attributes.project_id: "${projectId}"`, }) .set('kbn-xsrf', 'true') .expect(200); @@ -130,7 +130,7 @@ export default function ({ getService }: FtrProviderContext) { .get(`/s/${SPACE_ID}${SYNTHETICS_API_URLS.SYNTHETICS_MONITORS}`) .auth(username, password) .query({ - filter: `${syntheticsMonitorType}.attributes.journey_id: ${projectMonitors.monitors[0].id}`, + filter: `${syntheticsMonitorSavedObjectType}.attributes.journey_id: ${projectMonitors.monitors[0].id}`, }) .set('kbn-xsrf', 'true') .expect(200); @@ -185,7 +185,7 @@ export default function ({ getService }: FtrProviderContext) { .get(`/s/${SPACE_ID}${SYNTHETICS_API_URLS.SYNTHETICS_MONITORS}`) .auth(username, password) .query({ - filter: `${syntheticsMonitorType}.attributes.journey_id: ${projectMonitors.monitors[0].id}`, + filter: `${syntheticsMonitorSavedObjectType}.attributes.journey_id: ${projectMonitors.monitors[0].id}`, }) .set('kbn-xsrf', 'true') .expect(200); @@ -241,7 +241,7 @@ export default function ({ getService }: FtrProviderContext) { .get(`/s/${SPACE_ID}${SYNTHETICS_API_URLS.SYNTHETICS_MONITORS}`) .auth(username, password) .query({ - filter: `${syntheticsMonitorType}.attributes.journey_id: ${httpProjectMonitors.monitors[1].id}`, + filter: `${syntheticsMonitorSavedObjectType}.attributes.journey_id: ${httpProjectMonitors.monitors[1].id}`, }) .set('kbn-xsrf', 'true') .expect(200); @@ -403,7 +403,7 @@ export default function ({ getService }: FtrProviderContext) { .get(`/s/${SPACE_ID}${SYNTHETICS_API_URLS.SYNTHETICS_MONITORS}`) .auth(username, password) .query({ - filter: `${syntheticsMonitorType}.attributes.journey_id: ${projectMonitors.monitors[0].id}`, + filter: `${syntheticsMonitorSavedObjectType}.attributes.journey_id: ${projectMonitors.monitors[0].id}`, }) .set('kbn-xsrf', 'true') .expect(200); @@ -438,7 +438,7 @@ export default function ({ getService }: FtrProviderContext) { .get(`/s/${SPACE_ID}${SYNTHETICS_API_URLS.SYNTHETICS_MONITORS}`) .auth(username, password) .query({ - filter: `${syntheticsMonitorType}.attributes.journey_id: ${projectMonitors.monitors[0].id}`, + filter: `${syntheticsMonitorSavedObjectType}.attributes.journey_id: ${projectMonitors.monitors[0].id}`, }) .set('kbn-xsrf', 'true') .expect(200); @@ -496,7 +496,7 @@ export default function ({ getService }: FtrProviderContext) { .get(`/s/${SPACE_ID}${SYNTHETICS_API_URLS.SYNTHETICS_MONITORS}`) .auth(username, password) .query({ - filter: `${syntheticsMonitorType}.attributes.journey_id: ${projectMonitors.monitors[0].id}`, + filter: `${syntheticsMonitorSavedObjectType}.attributes.journey_id: ${projectMonitors.monitors[0].id}`, }) .set('kbn-xsrf', 'true') .expect(200); @@ -526,7 +526,7 @@ export default function ({ getService }: FtrProviderContext) { const response = await supertest .get(SYNTHETICS_API_URLS.SYNTHETICS_MONITORS) .query({ - filter: `${syntheticsMonitorType}.attributes.journey_id: ${projectMonitors.monitors[0].id}`, + filter: `${syntheticsMonitorSavedObjectType}.attributes.journey_id: ${projectMonitors.monitors[0].id}`, }) .set('kbn-xsrf', 'true') .expect(200); diff --git a/x-pack/solutions/observability/test/api_integration/apis/synthetics/services/synthetics_monitor_test_service.ts b/x-pack/solutions/observability/test/api_integration/apis/synthetics/services/synthetics_monitor_test_service.ts index d1dda60c8d7c..c959955a08b8 100644 --- a/x-pack/solutions/observability/test/api_integration/apis/synthetics/services/synthetics_monitor_test_service.ts +++ b/x-pack/solutions/observability/test/api_integration/apis/synthetics/services/synthetics_monitor_test_service.ts @@ -6,7 +6,7 @@ */ import { SYNTHETICS_API_URLS } from '@kbn/synthetics-plugin/common/constants'; -import { syntheticsMonitorType } from '@kbn/synthetics-plugin/common/types/saved_objects'; +import { legacySyntheticsMonitorTypeSingle } from '@kbn/synthetics-plugin/common/types/saved_objects'; import { EncryptedSyntheticsSavedMonitor } from '@kbn/synthetics-plugin/common/runtime_types'; import { MonitorInspectResponse } from '@kbn/synthetics-plugin/public/apps/synthetics/state/monitor_management/api'; import { v4 as uuidv4 } from 'uuid'; @@ -157,7 +157,7 @@ export class SyntheticsMonitorTestService { const response = await this.supertest .get(`/s/${space}${SYNTHETICS_API_URLS.SYNTHETICS_MONITORS}`) .query({ - filter: `${syntheticsMonitorType}.attributes.journey_id: "${journeyId}" AND ${syntheticsMonitorType}.attributes.project_id: "${projectId}"`, + filter: `${legacySyntheticsMonitorTypeSingle}.attributes.journey_id: "${journeyId}" AND ${legacySyntheticsMonitorTypeSingle}.attributes.project_id: "${projectId}"`, }) .set('kbn-xsrf', 'true') .expect(200); diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/synthetics/create_monitor.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/synthetics/create_monitor.ts index 7dc1eead87b1..5e418683e81c 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/synthetics/create_monitor.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/synthetics/create_monitor.ts @@ -30,14 +30,24 @@ export const addMonitorAPIHelper = async ( monitor: any, statusCode = 200, roleAuthc: RoleCredentials, - samlAuth: SamlAuthProviderType + samlAuth: SamlAuthProviderType, + gettingStarted?: boolean, + savedObjectType?: string ) => { + let queryParams = savedObjectType ? `savedObjectType=${savedObjectType}` : ''; + if (gettingStarted) { + queryParams = `?gettingStarted=true${queryParams}`; + } else if (queryParams) { + queryParams = `?${queryParams}`; + } + const result = await supertestAPI - .post(SYNTHETICS_API_URLS.SYNTHETICS_MONITORS) + .post(SYNTHETICS_API_URLS.SYNTHETICS_MONITORS + queryParams) .set(roleAuthc.apiKeyHeader) .set(samlAuth.getInternalRequestHeader()) - .send(monitor) - .expect(statusCode); + .send(monitor); + + expect(result.statusCode).eql(statusCode, JSON.stringify(result.body)); if (statusCode === 200) { const { created_at: createdAt, updated_at: updatedAt, id, config_id: configId } = result.body; @@ -135,7 +145,7 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { .post(`/s/${SPACE_ID}${SYNTHETICS_API_URLS.SYNTHETICS_MONITORS}`) .set(editorRoleAuthc.apiKeyHeader) .set(samlAuth.getInternalRequestHeader()) - .send(monitor) + .send({ ...monitor, spaces: [] }) .expect(200); monitorId = apiResponse.body.id; expect(apiResponse.body[ConfigKey.NAMESPACE]).eql(EXPECTED_NAMESPACE); diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/synthetics/create_monitor_private_location.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/synthetics/create_monitor_private_location.ts index df44d471e313..2ede65385538 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/synthetics/create_monitor_private_location.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/synthetics/create_monitor_private_location.ts @@ -30,7 +30,7 @@ import { addMonitorAPIHelper, keyToOmitList, omitMonitorKeys } from './create_mo import { SyntheticsMonitorTestService } from '../../../services/synthetics_monitor'; export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { - describe('PrivateLocationAddMonitor', function () { + describe('PrivateLocationCreateMonitor', function () { const kibanaServer = getService('kibanaServer'); const supertestWithoutAuth = getService('supertestWithoutAuth'); const supertestWithAuth = getService('supertest'); @@ -330,6 +330,7 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { name: `Test monitor ${uuidv4()}`, [ConfigKey.NAMESPACE]: 'default', locations: [spaceScopedPrivateLocation], + spaces: [SPACE_ID], }; await kibanaServer.spaces.create({ id: SPACE_ID, name: SPACE_NAME }); @@ -560,6 +561,7 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { omitMonitorKeys({ ...DEFAULT_FIELDS[MonitorTypeEnum.HTTP], ...newMonitor, + spaces: ['default'], }) ); }); @@ -641,6 +643,7 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { ...httpMonitorJson, [ConfigKey.NAMESPACE]: 'default', locations: [privateLocation], + spaces: [], }; let monitorId = ''; await kibanaServer.spaces.create({ id: SPACE_ID, name: SPACE_NAME }); @@ -667,6 +670,7 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { const monitor = { ...httpMonitorJson, locations: [privateLocation], + spaces: [], }; let monitorId = ''; diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/synthetics/create_monitor_project.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/synthetics/create_monitor_project.ts index 1c13d64af3ac..6527ba36da68 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/synthetics/create_monitor_project.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/synthetics/create_monitor_project.ts @@ -13,12 +13,15 @@ import { PrivateLocation, ServiceLocation, } from '@kbn/synthetics-plugin/common/runtime_types'; +import { + syntheticsMonitorSavedObjectType, + legacySyntheticsMonitorTypeSingle, +} from '@kbn/synthetics-plugin/common/types/saved_objects'; import { SYNTHETICS_API_URLS } from '@kbn/synthetics-plugin/common/constants'; import { PROFILE_VALUES_ENUM, PROFILES_MAP, } from '@kbn/synthetics-plugin/common/constants/monitor_defaults'; -import { syntheticsMonitorType } from '@kbn/synthetics-plugin/common/types/saved_objects'; import { DeploymentAgnosticFtrProviderContext } from '../../../ftr_provider_context'; import { getFixtureJson } from './helpers/get_fixture_json'; import { PrivateLocationTestService } from '../../../services/synthetics_private_location'; @@ -61,7 +64,7 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { const response = await supertest .get(`/s/${space}${SYNTHETICS_API_URLS.SYNTHETICS_MONITORS}`) .query({ - filter: `${syntheticsMonitorType}.attributes.journey_id: "${journeyId}" AND ${syntheticsMonitorType}.attributes.project_id: "${projectId}"`, + filter: `${syntheticsMonitorSavedObjectType}.attributes.journey_id: "${journeyId}" AND ${syntheticsMonitorSavedObjectType}.attributes.project_id: "${projectId}"`, }) .set(editorUser.apiKeyHeader) .set(samlAuth.getInternalRequestHeader()) @@ -155,7 +158,9 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { const journeyId = monitor.id; const createdMonitorsResponse = await supertest .get(SYNTHETICS_API_URLS.SYNTHETICS_MONITORS) - .query({ filter: `${syntheticsMonitorType}.attributes.journey_id: ${journeyId}` }) + .query({ + filter: `${syntheticsMonitorSavedObjectType}.attributes.journey_id: ${journeyId}`, + }) .set(editorUser.apiKeyHeader) .set(samlAuth.getInternalRequestHeader()) .expect(200); @@ -230,6 +235,7 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { created_at: decryptedCreatedMonitor.rawBody.created_at, labels: {}, maintenance_windows: [], + spaces: ['default'], }); } }); @@ -271,7 +277,9 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { const isTLSEnabled = Object.keys(monitor).some((key) => key.includes('ssl')); const createdMonitorsResponse = await supertest .get(SYNTHETICS_API_URLS.SYNTHETICS_MONITORS) - .query({ filter: `${syntheticsMonitorType}.attributes.journey_id: ${journeyId}` }) + .query({ + filter: `${syntheticsMonitorSavedObjectType}.attributes.journey_id: ${journeyId}`, + }) .set(editorUser.apiKeyHeader) .set(samlAuth.getInternalRequestHeader()) .expect(200); @@ -359,6 +367,7 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { max_attempts: 2, labels: {}, maintenance_windows: [], + spaces: ['default'], updated_at: decryptedCreatedMonitor.updated_at, created_at: decryptedCreatedMonitor.created_at, }); @@ -409,7 +418,9 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { const isTLSEnabled = Object.keys(monitor).some((key) => key.includes('ssl')); const createdMonitorsResponse = await supertest .get(SYNTHETICS_API_URLS.SYNTHETICS_MONITORS) - .query({ filter: `${syntheticsMonitorType}.attributes.journey_id: ${journeyId}` }) + .query({ + filter: `${syntheticsMonitorSavedObjectType}.attributes.journey_id: ${journeyId}`, + }) .set(editorUser.apiKeyHeader) .set(samlAuth.getInternalRequestHeader()) .expect(200); @@ -476,6 +487,7 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { max_attempts: 2, labels: {}, maintenance_windows: [], + spaces: ['default'], updated_at: decryptedCreatedMonitor.updated_at, created_at: decryptedCreatedMonitor.created_at, }); @@ -524,7 +536,9 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { const journeyId = monitor.id; const createdMonitorsResponse = await supertest .get(SYNTHETICS_API_URLS.SYNTHETICS_MONITORS) - .query({ filter: `${syntheticsMonitorType}.attributes.journey_id: ${journeyId}` }) + .query({ + filter: `${syntheticsMonitorSavedObjectType}.attributes.journey_id: ${journeyId}`, + }) .set(editorUser.apiKeyHeader) .set(samlAuth.getInternalRequestHeader()) .expect(200); @@ -582,6 +596,7 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { created_at: decryptedCreatedMonitor.created_at, labels: {}, maintenance_windows: [], + spaces: ['default'], }); } } finally { @@ -608,7 +623,7 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { const response = await supertest .get(SYNTHETICS_API_URLS.SYNTHETICS_MONITORS) .query({ - filter: `${syntheticsMonitorType}.attributes.journey_id: ${projectMonitors.monitors[0].id}`, + filter: `${syntheticsMonitorSavedObjectType}.attributes.journey_id: ${projectMonitors.monitors[0].id}`, }) .set(editorUser.apiKeyHeader) .set(samlAuth.getInternalRequestHeader()) @@ -661,7 +676,7 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { return supertest .get(SYNTHETICS_API_URLS.SYNTHETICS_MONITORS) .query({ - filter: `${syntheticsMonitorType}.attributes.journey_id: ${monitor.id}`, + filter: `${syntheticsMonitorSavedObjectType}.attributes.journey_id: ${monitor.id}`, internal: true, }) .set(editorUser.apiKeyHeader) @@ -783,5 +798,173 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { await deleteMonitor(httpProjectMonitors.monitors[1].id, project); } }); + + // --- Legacy monitor CRUD tests --- + describe('LegacyProjectMonitorCRUD', () => { + let legacyProject: string; + let legacyMonitor: any; + let legacyMonitorId: string; + + beforeEach(async () => { + legacyProject = `legacy-project-${uuidv4()}`; + legacyMonitorId = uuidv4(); + legacyMonitor = { + ...getFixtureJson('project_http_monitor').monitors[1], + id: legacyMonitorId, + name: `Legacy Monitor ${legacyMonitorId}`, + }; + await kibanaServer.savedObjects.clean({ + types: [legacySyntheticsMonitorTypeSingle], + }); + }); + + it('should create a legacy project monitor', async () => { + const { body } = await supertest + .put( + SYNTHETICS_API_URLS.SYNTHETICS_MONITORS_PROJECT_UPDATE.replace( + '{projectName}', + legacyProject + ) + `?savedObjectType=${legacySyntheticsMonitorTypeSingle}` + ) + .set(editorUser.apiKeyHeader) + .set(samlAuth.getInternalRequestHeader()) + .send({ monitors: [legacyMonitor] }) + .expect(200); + + expect(body).eql({ + updatedMonitors: [], + createdMonitors: [legacyMonitorId], + failedMonitors: [], + }); + + // Fetch from SO API to verify creation + const soRes = await kibanaServer.savedObjects.find({ + type: legacySyntheticsMonitorTypeSingle, + }); + const found = soRes.saved_objects.find( + (obj: any) => obj.attributes.journey_id === legacyMonitorId + ); + expect(found).not.to.be(undefined); + expect(found?.attributes.name).to.eql(`Legacy Monitor ${legacyMonitorId}`); + }); + + it('should fetch a legacy project monitor', async () => { + // Create first + await supertest + .put( + SYNTHETICS_API_URLS.SYNTHETICS_MONITORS_PROJECT_UPDATE.replace( + '{projectName}', + legacyProject + ) + `?savedObjectType=${legacySyntheticsMonitorTypeSingle}` + ) + .set(editorUser.apiKeyHeader) + .set(samlAuth.getInternalRequestHeader()) + .send({ monitors: [legacyMonitor] }) + .expect(200); + + // Fetch via monitors API + const res = await supertest + .get(SYNTHETICS_API_URLS.SYNTHETICS_MONITORS + '?internal=true') + .query({ + filter: `${legacySyntheticsMonitorTypeSingle}.attributes.journey_id: ${legacyMonitorId}`, + }) + .set(editorUser.apiKeyHeader) + .set(samlAuth.getInternalRequestHeader()) + .expect(200); + + expect(res.body.monitors.length).to.be(1); + expect(res.body.monitors[0].journey_id).to.eql(legacyMonitorId); + expect(res.body.monitors[0].name).to.eql(`Legacy Monitor ${legacyMonitorId}`); + }); + + it('should edit a legacy project monitor', async () => { + // Create first + await supertest + .put( + SYNTHETICS_API_URLS.SYNTHETICS_MONITORS_PROJECT_UPDATE.replace( + '{projectName}', + legacyProject + ) + `?savedObjectType=${legacySyntheticsMonitorTypeSingle}` + ) + .set(editorUser.apiKeyHeader) + .set(samlAuth.getInternalRequestHeader()) + .send({ monitors: [legacyMonitor] }) + .expect(200); + + // Edit via project update + const editedName = `Legacy Monitor Edited ${legacyMonitorId}`; + const editedMonitor = { ...legacyMonitor, name: editedName }; + const { body } = await supertest + .put( + SYNTHETICS_API_URLS.SYNTHETICS_MONITORS_PROJECT_UPDATE.replace( + '{projectName}', + legacyProject + ) + `?savedObjectType=${legacySyntheticsMonitorTypeSingle}` + ) + .set(editorUser.apiKeyHeader) + .set(samlAuth.getInternalRequestHeader()) + .send({ monitors: [editedMonitor] }) + .expect(200); + + expect(body).eql({ + updatedMonitors: [legacyMonitorId], + createdMonitors: [], + failedMonitors: [], + }); + + // Fetch and verify edit + const soRes = await kibanaServer.savedObjects.find({ + type: legacySyntheticsMonitorTypeSingle, + }); + const found = soRes.saved_objects.find( + (obj: any) => obj.attributes.journey_id === legacyMonitorId + ); + expect(found?.attributes.name).to.eql(editedName); + }); + + it('should delete a legacy project monitor', async () => { + // Create first + await supertest + .put( + SYNTHETICS_API_URLS.SYNTHETICS_MONITORS_PROJECT_UPDATE.replace( + '{projectName}', + legacyProject + ) + `?savedObjectType=${legacySyntheticsMonitorTypeSingle}` + ) + .set(editorUser.apiKeyHeader) + .set(samlAuth.getInternalRequestHeader()) + .send({ monitors: [legacyMonitor] }) + .expect(200); + + // Delete via SYNTHETICS_MONITORS_PROJECT_DELETE API + const soRes = await kibanaServer.savedObjects.find({ + type: legacySyntheticsMonitorTypeSingle, + }); + const found = soRes.saved_objects.find( + (obj: any) => obj.attributes.journey_id === legacyMonitorId + ); + expect(found).not.to.be(undefined); + + // Use the project delete API for legacy monitor + await supertest + .delete( + SYNTHETICS_API_URLS.SYNTHETICS_MONITORS_PROJECT_DELETE.replace( + '{projectName}', + legacyProject + ) + ) + .set(editorUser.apiKeyHeader) + .set(samlAuth.getInternalRequestHeader()) + .send({ monitors: [legacyMonitorId] }) + .expect(200); + + // Ensure deleted + const soResAfter = await kibanaServer.savedObjects.find({ + type: legacySyntheticsMonitorTypeSingle, + }); + const foundAfter = soResAfter.saved_objects.find((obj: any) => obj.id === found!.id); + expect(foundAfter).to.be(undefined); + }); + }); }); } diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/synthetics/create_monitor_project_private_location.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/synthetics/create_monitor_project_private_location.ts index 5e8415fe5fb2..674ddc6d36c0 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/synthetics/create_monitor_project_private_location.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/synthetics/create_monitor_project_private_location.ts @@ -11,7 +11,7 @@ import { RoleCredentials } from '@kbn/ftr-common-functional-services'; import { PackagePolicy } from '@kbn/fleet-plugin/common'; import { ProjectMonitorsRequest, ConfigKey } from '@kbn/synthetics-plugin/common/runtime_types'; import { SYNTHETICS_API_URLS } from '@kbn/synthetics-plugin/common/constants'; -import { syntheticsMonitorType } from '@kbn/synthetics-plugin/common/types/saved_objects'; +import { syntheticsMonitorSavedObjectType } from '@kbn/synthetics-plugin/common/types/saved_objects'; import { REQUEST_TOO_LARGE } from '@kbn/synthetics-plugin/server/routes/monitor_cruds/project_monitor/add_monitor_project'; import { PROFILE_VALUES_ENUM, @@ -216,7 +216,9 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { const journeyId = monitor.id; const createdMonitorsResponse = await supertestWithoutAuth .get(SYNTHETICS_API_URLS.SYNTHETICS_MONITORS) - .query({ filter: `${syntheticsMonitorType}.attributes.journey_id: ${journeyId}` }) + .query({ + filter: `${syntheticsMonitorSavedObjectType}.attributes.journey_id: ${journeyId}`, + }) .set(editorUser.apiKeyHeader) .set(samlAuth.getInternalRequestHeader()) .expect(200); @@ -302,6 +304,7 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { created_at: decryptedCreatedMonitor.rawBody.created_at, labels: {}, maintenance_windows: [], + spaces: ['default'], }); } }); @@ -331,7 +334,9 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { const journeyId = monitor.id; const createdMonitorsResponse = await supertestWithoutAuth .get(SYNTHETICS_API_URLS.SYNTHETICS_MONITORS) - .query({ filter: `${syntheticsMonitorType}.attributes.journey_id: ${journeyId}` }) + .query({ + filter: `${syntheticsMonitorSavedObjectType}.attributes.journey_id: ${journeyId}`, + }) .set(editorUser.apiKeyHeader) .set(samlAuth.getInternalRequestHeader()) .expect(200); @@ -385,7 +390,9 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { const isTLSEnabled = Object.keys(monitor).some((key) => key.includes('ssl')); const createdMonitorsResponse = await supertestWithoutAuth .get(SYNTHETICS_API_URLS.SYNTHETICS_MONITORS) - .query({ filter: `${syntheticsMonitorType}.attributes.journey_id: ${journeyId}` }) + .query({ + filter: `${syntheticsMonitorSavedObjectType}.attributes.journey_id: ${journeyId}`, + }) .set(editorUser.apiKeyHeader) .set(samlAuth.getInternalRequestHeader()) .expect(200); @@ -484,6 +491,7 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { max_attempts: 2, labels: {}, maintenance_windows: [], + spaces: ['default'], updated_at: decryptedCreatedMonitor.updated_at, created_at: decryptedCreatedMonitor.created_at, }); @@ -526,7 +534,9 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { const isTLSEnabled = Object.keys(monitor).some((key) => key.includes('ssl')); const createdMonitorsResponse = await supertestWithoutAuth .get(SYNTHETICS_API_URLS.SYNTHETICS_MONITORS) - .query({ filter: `${syntheticsMonitorType}.attributes.journey_id: ${journeyId}` }) + .query({ + filter: `${syntheticsMonitorSavedObjectType}.attributes.journey_id: ${journeyId}`, + }) .set(editorUser.apiKeyHeader) .set(samlAuth.getInternalRequestHeader()) .expect(200); @@ -604,6 +614,7 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { max_attempts: 2, labels: {}, maintenance_windows: [], + spaces: ['default'], updated_at: decryptedCreatedMonitor.updated_at, created_at: decryptedCreatedMonitor.created_at, }); @@ -644,7 +655,9 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { const journeyId = monitor.id; const createdMonitorsResponse = await supertestWithoutAuth .get(SYNTHETICS_API_URLS.SYNTHETICS_MONITORS) - .query({ filter: `${syntheticsMonitorType}.attributes.journey_id: ${journeyId}` }) + .query({ + filter: `${syntheticsMonitorSavedObjectType}.attributes.journey_id: ${journeyId}`, + }) .set(editorUser.apiKeyHeader) .set(samlAuth.getInternalRequestHeader()) .expect(200); @@ -713,6 +726,7 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { created_at: decryptedCreatedMonitor.created_at, labels: {}, maintenance_windows: [], + spaces: ['default'], }); } }); @@ -843,7 +857,7 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { const getResponse = await supertestWithoutAuth .get(`/s/${SPACE_ID}${SYNTHETICS_API_URLS.SYNTHETICS_MONITORS}`) .query({ - filter: `${syntheticsMonitorType}.attributes.journey_id: ${projectMonitors.monitors[0].id}`, + filter: `${syntheticsMonitorSavedObjectType}.attributes.journey_id: ${projectMonitors.monitors[0].id}`, }) .set(editorUser.apiKeyHeader) .set(samlAuth.getInternalRequestHeader()) @@ -887,7 +901,7 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { .set(editorUser.apiKeyHeader) .set(samlAuth.getInternalRequestHeader()) .query({ - filter: `${syntheticsMonitorType}.attributes.journey_id: ${projectMonitors.monitors[0].id}`, + filter: `${syntheticsMonitorSavedObjectType}.attributes.journey_id: ${projectMonitors.monitors[0].id}`, }) .expect(200); const { monitors } = getResponse.body; @@ -930,7 +944,7 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { .set(editorUser.apiKeyHeader) .set(samlAuth.getInternalRequestHeader()) .query({ - filter: `${syntheticsMonitorType}.attributes.journey_id: ${httpProjectMonitors.monitors[1].id}`, + filter: `${syntheticsMonitorSavedObjectType}.attributes.journey_id: ${httpProjectMonitors.monitors[1].id}`, }) .set('kbn-xsrf', 'true') .expect(200); @@ -1053,7 +1067,7 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { .set(editorUser.apiKeyHeader) .set(samlAuth.getInternalRequestHeader()) .query({ - filter: `${syntheticsMonitorType}.attributes.journey_id: ${projectMonitors.monitors[0].id}`, + filter: `${syntheticsMonitorSavedObjectType}.attributes.journey_id: ${projectMonitors.monitors[0].id}`, }) .set('kbn-xsrf', 'true') .expect(200); @@ -1095,7 +1109,7 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { .set(editorUser.apiKeyHeader) .set(samlAuth.getInternalRequestHeader()) .query({ - filter: `${syntheticsMonitorType}.attributes.journey_id: ${projectMonitors.monitors[0].id}`, + filter: `${syntheticsMonitorSavedObjectType}.attributes.journey_id: ${projectMonitors.monitors[0].id}`, }) .expect(200); const { monitors: monitorsUpdated } = getResponseUpdated.body; @@ -1139,7 +1153,7 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { .set(editorUser.apiKeyHeader) .set(samlAuth.getInternalRequestHeader()) .query({ - filter: `${syntheticsMonitorType}.attributes.journey_id: ${projectMonitors.monitors[0].id}`, + filter: `${syntheticsMonitorSavedObjectType}.attributes.journey_id: ${projectMonitors.monitors[0].id}`, }) .expect(200); const { monitors } = getResponse.body; @@ -1163,7 +1177,7 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { const response = await supertestWithoutAuth .get(SYNTHETICS_API_URLS.SYNTHETICS_MONITORS) .query({ - filter: `${syntheticsMonitorType}.attributes.journey_id: ${projectMonitors.monitors[0].id}`, + filter: `${syntheticsMonitorSavedObjectType}.attributes.journey_id: ${projectMonitors.monitors[0].id}`, }) .set(editorUser.apiKeyHeader) .set(samlAuth.getInternalRequestHeader()) @@ -1221,7 +1235,7 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { const response = await supertestWithoutAuth .get(SYNTHETICS_API_URLS.SYNTHETICS_MONITORS) .query({ - filter: `${syntheticsMonitorType}.attributes.journey_id: ${projectMonitors.monitors[0].id}`, + filter: `${syntheticsMonitorSavedObjectType}.attributes.journey_id: ${projectMonitors.monitors[0].id}`, }) .set(editorUser.apiKeyHeader) .set(samlAuth.getInternalRequestHeader()) @@ -1269,7 +1283,7 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { const monitorsResponse = await supertestWithoutAuth .get(SYNTHETICS_API_URLS.SYNTHETICS_MONITORS) .query({ - filter: `${syntheticsMonitorType}.attributes.journey_id: ${projectMonitors.monitors[0].id}`, + filter: `${syntheticsMonitorSavedObjectType}.attributes.journey_id: ${projectMonitors.monitors[0].id}`, }) .set(editorUser.apiKeyHeader) .set(samlAuth.getInternalRequestHeader()) @@ -1330,7 +1344,7 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { const monitorsResponse = await supertestWithoutAuth .get(SYNTHETICS_API_URLS.SYNTHETICS_MONITORS) .query({ - filter: `${syntheticsMonitorType}.attributes.journey_id: ${httpProjectMonitors.monitors[1].id}`, + filter: `${syntheticsMonitorSavedObjectType}.attributes.journey_id: ${httpProjectMonitors.monitors[1].id}`, }) .set(editorUser.apiKeyHeader) .set(samlAuth.getInternalRequestHeader()) @@ -1391,7 +1405,7 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { const monitorsResponse = await supertestWithoutAuth .get(SYNTHETICS_API_URLS.SYNTHETICS_MONITORS) .query({ - filter: `${syntheticsMonitorType}.attributes.journey_id: ${monitorRequest.monitors[0].id}`, + filter: `${syntheticsMonitorSavedObjectType}.attributes.journey_id: ${monitorRequest.monitors[0].id}`, }) .set(editorUser.apiKeyHeader) .set(samlAuth.getInternalRequestHeader()) @@ -1458,7 +1472,7 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { const monitorsResponse = await supertestWithoutAuth .get(SYNTHETICS_API_URLS.SYNTHETICS_MONITORS) .query({ - filter: `${syntheticsMonitorType}.attributes.journey_id: ${projectMonitors.monitors[0].id}`, + filter: `${syntheticsMonitorSavedObjectType}.attributes.journey_id: ${projectMonitors.monitors[0].id}`, }) .set(editorUser.apiKeyHeader) .set(samlAuth.getInternalRequestHeader()) @@ -1539,7 +1553,7 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { const monitorsResponse = await supertestWithoutAuth .get(SYNTHETICS_API_URLS.SYNTHETICS_MONITORS) .query({ - filter: `${syntheticsMonitorType}.attributes.journey_id: ${projectMonitors.monitors[0].id}`, + filter: `${syntheticsMonitorSavedObjectType}.attributes.journey_id: ${projectMonitors.monitors[0].id}`, }) .set(editorUser.apiKeyHeader) .set(samlAuth.getInternalRequestHeader()) @@ -1756,7 +1770,7 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { const getResponse = await supertestWithoutAuth .get(`${SYNTHETICS_API_URLS.SYNTHETICS_MONITORS}`) .query({ - filter: `${syntheticsMonitorType}.attributes.journey_id: ${httpProjectMonitors.monitors[1].id}`, + filter: `${syntheticsMonitorSavedObjectType}.attributes.journey_id: ${httpProjectMonitors.monitors[1].id}`, }) .set(editorUser.apiKeyHeader) .set(samlAuth.getInternalRequestHeader()) diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/synthetics/create_monitor_public_api.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/synthetics/create_monitor_public_api.ts index 3b603fcf74e3..697fdec20c13 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/synthetics/create_monitor_public_api.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/synthetics/create_monitor_public_api.ts @@ -68,6 +68,7 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { ...monitor, locations: [LOCAL_PUBLIC_LOCATION], name: 'https://www.google.com', + spaces: ['default'], }) ); }); @@ -90,6 +91,7 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { locations: [LOCAL_PUBLIC_LOCATION], name, retest_on_failure: true, + spaces: ['default'], }) ); }); @@ -113,6 +115,7 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { name, max_attempts: 1, retest_on_failure: undefined, // this key is not part of the SO and should not be defined + spaces: ['default'], }) ); }); @@ -135,6 +138,7 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { ...monitor, locations: [LOCAL_PUBLIC_LOCATION], name: 'https://www.google.com/', + spaces: ['default'], }) ); }); @@ -157,6 +161,7 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { ...monitor, locations: [LOCAL_PUBLIC_LOCATION], name: 'https://8.8.8.8', + spaces: ['default'], }) ); }); @@ -198,6 +203,7 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { ...defaultFields, ...monitor, locations: [LOCAL_PUBLIC_LOCATION], + spaces: ['default'], }) ); }); @@ -216,6 +222,7 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { ...defaultFields, ...monitor, locations: [LOCAL_PUBLIC_LOCATION], + spaces: ['default'], }) ); }); diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/synthetics/create_monitor_public_api_private_location.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/synthetics/create_monitor_public_api_private_location.ts index d301d7750481..4f37242f7017 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/synthetics/create_monitor_public_api_private_location.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/synthetics/create_monitor_public_api_private_location.ts @@ -124,6 +124,7 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { ...monitor, locations: [privateLocation], name: 'https://www.google.com', + spaces: ['default'], }) ); }); @@ -146,6 +147,7 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { locations: [privateLocation], name, retest_on_failure: true, + spaces: ['default'], }) ); }); @@ -169,6 +171,7 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { name, max_attempts: 1, retest_on_failure: undefined, // this key is not part of the SO and should not be defined + spaces: ['default'], }) ); }); @@ -191,6 +194,7 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { ...monitor, locations: [privateLocation], name: 'https://www.google.com/', + spaces: ['default'], }) ); }); @@ -213,6 +217,7 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { ...monitor, locations: [privateLocation], name: 'https://8.8.8.8', + spaces: ['default'], }) ); }); @@ -254,6 +259,7 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { ...defaultFields, ...monitor, locations: [privateLocation], + spaces: ['default'], }) ); }); @@ -272,6 +278,7 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { ...defaultFields, ...monitor, locations: [privateLocation], + spaces: ['default'], }) ); }); diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/synthetics/delete_monitor_project.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/synthetics/delete_monitor_project.ts index dd0cd7aa9a55..38270fd9e4b8 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/synthetics/delete_monitor_project.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/synthetics/delete_monitor_project.ts @@ -15,7 +15,7 @@ import { REQUEST_TOO_LARGE_DELETE } from '@kbn/synthetics-plugin/server/routes/m import { SYNTHETICS_API_URLS } from '@kbn/synthetics-plugin/common/constants'; import { PackagePolicy } from '@kbn/fleet-plugin/common'; import expect from '@kbn/expect'; -import { syntheticsMonitorType } from '@kbn/synthetics-plugin/common/types/saved_objects'; +import { syntheticsMonitorSavedObjectType } from '@kbn/synthetics-plugin/common/types/saved_objects'; import { DeploymentAgnosticFtrProviderContext } from '../../../ftr_provider_context'; import { getFixtureJson } from './helpers/get_fixture_json'; import { PrivateLocationTestService } from '../../../services/synthetics_private_location'; @@ -95,7 +95,7 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { const savedObjectsResponse = await supertest .get(SYNTHETICS_API_URLS.SYNTHETICS_MONITORS) .query({ - filter: `${syntheticsMonitorType}.attributes.project_id: "${project}"`, + filter: `${syntheticsMonitorSavedObjectType}.attributes.project_id: "${project}"`, }) .set(editorUser.apiKeyHeader) .set(samlAuth.getInternalRequestHeader()) @@ -157,7 +157,7 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { const savedObjectsResponse = await supertest .get(SYNTHETICS_API_URLS.SYNTHETICS_MONITORS) .query({ - filter: `${syntheticsMonitorType}.attributes.project_id: "${project}"`, + filter: `${syntheticsMonitorSavedObjectType}.attributes.project_id: "${project}"`, }) .set(editorUser.apiKeyHeader) .set(samlAuth.getInternalRequestHeader()) @@ -180,7 +180,7 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { const responseAfterDeletion = await supertest .get(SYNTHETICS_API_URLS.SYNTHETICS_MONITORS) .query({ - filter: `${syntheticsMonitorType}.attributes.project_id: "${project}"`, + filter: `${syntheticsMonitorSavedObjectType}.attributes.project_id: "${project}"`, }) .set(editorUser.apiKeyHeader) .set(samlAuth.getInternalRequestHeader()) @@ -230,7 +230,7 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { const savedObjectsResponse = await supertest .get(SYNTHETICS_API_URLS.SYNTHETICS_MONITORS) .query({ - filter: `${syntheticsMonitorType}.attributes.project_id: "${project}"`, + filter: `${syntheticsMonitorSavedObjectType}.attributes.project_id: "${project}"`, }) .set(editorUser.apiKeyHeader) .set(samlAuth.getInternalRequestHeader()) @@ -238,7 +238,7 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { const secondProjectSavedObjectResponse = await supertest .get(SYNTHETICS_API_URLS.SYNTHETICS_MONITORS) .query({ - filter: `${syntheticsMonitorType}.attributes.project_id: "${secondProject}"`, + filter: `${syntheticsMonitorSavedObjectType}.attributes.project_id: "${secondProject}"`, }) .set(editorUser.apiKeyHeader) .set(samlAuth.getInternalRequestHeader()) @@ -263,7 +263,7 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { const responseAfterDeletion = await supertest .get(SYNTHETICS_API_URLS.SYNTHETICS_MONITORS) .query({ - filter: `${syntheticsMonitorType}.attributes.project_id: "${project}"`, + filter: `${syntheticsMonitorSavedObjectType}.attributes.project_id: "${project}"`, }) .set(editorUser.apiKeyHeader) .set(samlAuth.getInternalRequestHeader()) @@ -271,7 +271,7 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { const secondResponseAfterDeletion = await supertest .get(SYNTHETICS_API_URLS.SYNTHETICS_MONITORS) .query({ - filter: `${syntheticsMonitorType}.attributes.project_id: "${secondProject}"`, + filter: `${syntheticsMonitorSavedObjectType}.attributes.project_id: "${secondProject}"`, }) .set(editorUser.apiKeyHeader) .set(samlAuth.getInternalRequestHeader()) @@ -350,7 +350,7 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { const savedObjectsResponse = await supertest .get(SYNTHETICS_API_URLS.SYNTHETICS_MONITORS) .query({ - filter: `${syntheticsMonitorType}.attributes.project_id: "${project}"`, + filter: `${syntheticsMonitorSavedObjectType}.attributes.project_id: "${project}"`, }) .set(editorUser.apiKeyHeader) .set(samlAuth.getInternalRequestHeader()) @@ -358,7 +358,7 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { const secondSpaceProjectSavedObjectResponse = await supertest .get(`/s/${SPACE_ID}${SYNTHETICS_API_URLS.SYNTHETICS_MONITORS}`) .query({ - filter: `${syntheticsMonitorType}.attributes.project_id: "${project}"`, + filter: `${syntheticsMonitorSavedObjectType}.attributes.project_id: "${project}"`, }) .set(editorUser.apiKeyHeader) .set(samlAuth.getInternalRequestHeader()) @@ -387,7 +387,7 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { const responseAfterDeletion = await supertest .get(SYNTHETICS_API_URLS.SYNTHETICS_MONITORS) .query({ - filter: `${syntheticsMonitorType}.attributes.project_id: "${project}"`, + filter: `${syntheticsMonitorSavedObjectType}.attributes.project_id: "${project}"`, }) .set(editorUser.apiKeyHeader) .set(samlAuth.getInternalRequestHeader()) @@ -395,7 +395,7 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { const secondSpaceResponseAfterDeletion = await supertest .get(`/s/${SPACE_ID}${SYNTHETICS_API_URLS.SYNTHETICS_MONITORS}`) .query({ - filter: `${syntheticsMonitorType}.attributes.project_id: "${project}"`, + filter: `${syntheticsMonitorSavedObjectType}.attributes.project_id: "${project}"`, }) .set(editorUser.apiKeyHeader) .set(samlAuth.getInternalRequestHeader()) @@ -449,7 +449,7 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { const savedObjectsResponse = await supertest .get(SYNTHETICS_API_URLS.SYNTHETICS_MONITORS) .query({ - filter: `${syntheticsMonitorType}.attributes.project_id: "${project}"`, + filter: `${syntheticsMonitorSavedObjectType}.attributes.project_id: "${project}"`, }) .set(editorUser.apiKeyHeader) .set(samlAuth.getInternalRequestHeader()) @@ -485,7 +485,7 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { const responseAfterDeletion = await supertest .get(SYNTHETICS_API_URLS.SYNTHETICS_MONITORS) .query({ - filter: `${syntheticsMonitorType}.attributes.project_id: "${project}"`, + filter: `${syntheticsMonitorSavedObjectType}.attributes.project_id: "${project}"`, }) .set(editorUser.apiKeyHeader) .set(samlAuth.getInternalRequestHeader()) diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/synthetics/edit_monitor_private_location.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/synthetics/edit_monitor_private_location.ts index 986f28c01ebf..e27523414fe1 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/synthetics/edit_monitor_private_location.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/synthetics/edit_monitor_private_location.ts @@ -47,8 +47,9 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { .post(apiURL + '?internal=true') .set(editorUser.apiKeyHeader) .set(samlAuth.getInternalRequestHeader()) - .send(monitor) - .expect(200); + .send(monitor); + + expect(res.status).eql(200, JSON.stringify(res.body)); const { url, created_at: createdAt, updated_at: updatedAt, ...rest } = res.body; diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/synthetics/edit_monitor_public_api.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/synthetics/edit_monitor_public_api.ts index 7a99e27c9e40..30d4bd5db44d 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/synthetics/edit_monitor_public_api.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/synthetics/edit_monitor_public_api.ts @@ -96,6 +96,7 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { ...monitor, locations: [LOCAL_PUBLIC_LOCATION], name: 'https://www.google.com', + spaces: ['default'], }) ); }); @@ -117,6 +118,7 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { locations: [LOCAL_PUBLIC_LOCATION], revision: 2, url: 'https://www.google.com', + spaces: ['default'], }) ); }); @@ -144,6 +146,7 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { ...monitor, locations: [LOCAL_PUBLIC_LOCATION], name: 'test name', + spaces: ['default'], }) ); @@ -182,6 +185,7 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { LOCAL_PUBLIC_LOCATION, { ...LOCAL_PUBLIC_LOCATION, id: 'dev2', label: 'Dev Service 2' }, ], + spaces: ['default'], }) ); }); @@ -200,6 +204,7 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { revision: 4, url: 'https://www.google.com', locations: [{ ...LOCAL_PUBLIC_LOCATION, id: 'dev2', label: 'Dev Service 2' }], + spaces: ['default'], }) ); }); diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/synthetics/edit_monitor_public_api_private_location.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/synthetics/edit_monitor_public_api_private_location.ts index ded8106d6a66..e0938cec38c5 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/synthetics/edit_monitor_public_api_private_location.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/synthetics/edit_monitor_public_api_private_location.ts @@ -101,6 +101,7 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { ...monitor, locations: [privateLocation1], name: 'https://www.google.com', + spaces: ['default'], }) ); }); @@ -203,6 +204,7 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { locations: [privateLocation1], revision: 2, url: 'https://www.google.com', + spaces: ['default'], }) ); }); @@ -230,6 +232,7 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { ...monitor, locations: [privateLocation1], name: 'test name', + spaces: ['default'], }) ); @@ -265,6 +268,7 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { revision: 3, url: 'https://www.google.com', locations: [omit(privateLocation1, 'spaces'), omit(privateLocation2, 'spaces')], + spaces: ['default'], }) ); }); @@ -283,6 +287,7 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { revision: 4, url: 'https://www.google.com', locations: [omit(privateLocation2, 'spaces')], + spaces: ['default'], }) ); }); diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/synthetics/enable_default_alerting.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/synthetics/enable_default_alerting.ts index 5981bc897461..f542f1fba4fd 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/synthetics/enable_default_alerting.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/synthetics/enable_default_alerting.ts @@ -31,8 +31,8 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { const privateLocationTestService = new PrivateLocationTestService(getService); - const addMonitorAPI = async (monitor: any, statusCode = 200) => { - return addMonitorAPIHelper(supertest, monitor, statusCode, editorUser, samlAuth); + const addMonitorAPI = async (monitor: any, gettingStarted?: boolean) => { + return addMonitorAPIHelper(supertest, monitor, 200, editorUser, samlAuth, gettingStarted); }; after(async () => { @@ -96,7 +96,7 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { it('enables alert when new monitor is added', async () => { const newMonitor = httpMonitorJson; - const { body: apiResponse } = await addMonitorAPI(newMonitor); + const { body: apiResponse } = await addMonitorAPI(newMonitor, true); expect(apiResponse).eql(omitMonitorKeys({ ...newMonitor, spaceId: 'default' })); @@ -115,7 +115,7 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { it('deletes (and recreates) the default rule when settings are updated', async () => { const newMonitor = httpMonitorJson; - const { body: apiResponse } = await addMonitorAPI(newMonitor); + const { body: apiResponse } = await addMonitorAPI(newMonitor, true); expect(apiResponse).eql(omitMonitorKeys(newMonitor)); @@ -194,7 +194,7 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { it('doesnt throw errors when rule has already been deleted', async () => { const newMonitor = httpMonitorJson; - const { body: apiResponse } = await addMonitorAPI(newMonitor); + const { body: apiResponse } = await addMonitorAPI(newMonitor, true); expect(apiResponse).eql(omitMonitorKeys(newMonitor)); diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/synthetics/fixtures/http_monitor.json b/x-pack/test/api_integration/deployment_agnostic/apis/observability/synthetics/fixtures/http_monitor.json index ad8b8ee345aa..68f135f7b2b3 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/synthetics/fixtures/http_monitor.json +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/synthetics/fixtures/http_monitor.json @@ -80,5 +80,6 @@ "ipv6": true, "params": "", "labels": {}, - "maintenance_windows": [] + "maintenance_windows": [], + "spaces": ["default"] } diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/synthetics/get_filters.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/synthetics/get_filters.ts index cd6f8ff2f727..a59535f66b5f 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/synthetics/get_filters.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/synthetics/get_filters.ts @@ -9,7 +9,7 @@ import { SYNTHETICS_API_URLS } from '@kbn/synthetics-plugin/common/constants'; import { RoleCredentials } from '@kbn/ftr-common-functional-services'; import expect from '@kbn/expect'; import { PrivateLocation } from '@kbn/synthetics-plugin/common/runtime_types'; -import { syntheticsMonitorType } from '@kbn/synthetics-plugin/common/types/saved_objects'; +import { syntheticsMonitorSavedObjectType } from '@kbn/synthetics-plugin/common/types/saved_objects'; import { DeploymentAgnosticFtrProviderContext } from '../../../ftr_provider_context'; import { PrivateLocationTestService } from '../../../services/synthetics_private_location'; @@ -25,11 +25,11 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { let privateLocation: PrivateLocation; after(async () => { - await kibanaServer.savedObjects.clean({ types: [syntheticsMonitorType] }); + await kibanaServer.savedObjects.clean({ types: [syntheticsMonitorSavedObjectType] }); }); before(async () => { - await kibanaServer.savedObjects.clean({ types: [syntheticsMonitorType] }); + await kibanaServer.savedObjects.clean({ types: [syntheticsMonitorSavedObjectType] }); editorUser = await samlAuth.createM2mApiKeyWithRoleScope('editor'); privateLocation = await privateLocationTestService.addTestPrivateLocation(); }); diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/synthetics/get_monitor.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/synthetics/get_monitor.ts index 4fa66f767c67..8afd57870096 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/synthetics/get_monitor.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/synthetics/get_monitor.ts @@ -233,7 +233,12 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { for (const mon of allMonitors) { await saveMonitor( - { ...mon, name: mon.name + Date.now(), locations: [spaceScopedPrivateLocation] }, + { + ...mon, + name: mon.name + Date.now(), + locations: [spaceScopedPrivateLocation], + spaces: [], + }, SPACE_ID ); } @@ -292,6 +297,7 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { revision: 1, locations: [privateLocation], name: `${monitors[0].name}-${uuid}-0`, + spaces: ['default'], }) ); }); @@ -323,6 +329,7 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { max_attempts: 2, labels: {}, maintenance_windows: [], + spaces: ['default'], }, ['config_id', 'id', 'form_monitor_type'] ) diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/synthetics/get_private_location_monitors.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/synthetics/get_private_location_monitors.ts new file mode 100644 index 000000000000..139ac1530cf7 --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/synthetics/get_private_location_monitors.ts @@ -0,0 +1,126 @@ +/* + * 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 { RoleCredentials } from '@kbn/ftr-common-functional-services'; +import { PrivateLocation, ServiceLocation } from '@kbn/synthetics-plugin/common/runtime_types'; +import { SYNTHETICS_API_URLS } from '@kbn/synthetics-plugin/common/constants'; +import expect from '@kbn/expect'; +import rawExpect from 'expect'; +import { DeploymentAgnosticFtrProviderContext } from '../../../ftr_provider_context'; +import { getFixtureJson } from './helpers/get_fixture_json'; +import { PrivateLocationTestService } from '../../../services/synthetics_private_location'; +import { addMonitorAPIHelper, omitMonitorKeys } from './create_monitor'; +import { SupertestWithRoleScopeType } from '../../../services'; + +export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { + describe('GetPrivateLocationMonitors', function () { + const kibanaServer = getService('kibanaServer'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); + const samlAuth = getService('samlAuth'); + const roleScopedSupertest = getService('roleScopedSupertest'); + let supertestEditorWithApiKey: SupertestWithRoleScopeType; + + let testFleetPolicyID: string; + let editorUser: RoleCredentials; + let privateLocations: PrivateLocation[] = []; + const testPolicyName = 'Fleet test server policy' + Date.now(); + + let newMonitor: { id: string; name: string }; + const testPrivateLocations = new PrivateLocationTestService(getService); + + before(async () => { + supertestEditorWithApiKey = await roleScopedSupertest.getSupertestWithRoleScope('editor', { + withInternalHeaders: true, + }); + + await kibanaServer.savedObjects.cleanStandardList(); + await testPrivateLocations.installSyntheticsPackage(); + editorUser = await samlAuth.createM2mApiKeyWithRoleScope('editor'); + }); + + after(async () => { + await supertestEditorWithApiKey.destroy(); + await samlAuth.invalidateM2mApiKeyWithRoleScope(editorUser); + await kibanaServer.savedObjects.cleanStandardList(); + }); + + it('adds a test fleet policy', async () => { + const apiResponse = await testPrivateLocations.addFleetPolicy(testPolicyName); + testFleetPolicyID = apiResponse.body.item.id; + }); + + it('add a test private location', async () => { + privateLocations = await testPrivateLocations.setTestLocations([testFleetPolicyID]); + + const apiResponse = await supertestEditorWithApiKey + .get(SYNTHETICS_API_URLS.SERVICE_LOCATIONS) + .expect(200); + + const testResponse: Array = [ + { + id: testFleetPolicyID, + isServiceManaged: false, + isInvalid: false, + label: privateLocations[0].label, + geo: { + lat: 0, + lon: 0, + }, + agentPolicyId: testFleetPolicyID, + spaces: ['default'], + }, + ]; + + rawExpect(apiResponse.body.locations).toEqual(rawExpect.arrayContaining(testResponse)); + }); + + it('adds a monitor in private location', async () => { + newMonitor = { + ...getFixtureJson('http_monitor'), + namespace: 'default', + locations: [privateLocations[0]], + }; + + const { body, rawBody } = await addMonitorAPIHelper( + supertestWithoutAuth, + newMonitor, + 200, + editorUser, + samlAuth + ); + expect(body).eql(omitMonitorKeys(newMonitor)); + newMonitor.id = rawBody.id; + newMonitor = { + ...getFixtureJson('http_monitor'), + namespace: 'default', + locations: [privateLocations[0]], + name: 'Monitor in private location', + }; + + // add a legacy monitor + const resp = await addMonitorAPIHelper( + supertestWithoutAuth, + newMonitor, + 200, + editorUser, + samlAuth, + undefined, + 'synthetics-monitor' + ); + expect(resp.body).eql(omitMonitorKeys(newMonitor)); + }); + + it('returns monitors for private location', async () => { + const apiResponse = await supertestEditorWithApiKey + .get(SYNTHETICS_API_URLS.PRIVATE_LOCATIONS_MONITORS) + .set('kbn-xsrf', 'true') + .expect(200); + + expect(apiResponse.body).to.eql([{ id: privateLocations[0].id, count: 2 }]); + }); + }); +} diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/synthetics/index.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/synthetics/index.ts index 2dd2e0b50812..c5275d7b962f 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/synthetics/index.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/synthetics/index.ts @@ -9,6 +9,7 @@ import { DeploymentAgnosticFtrProviderContext } from '../../../ftr_provider_cont export default function ({ loadTestFile }: DeploymentAgnosticFtrProviderContext) { describe('SyntheticsAPITests', () => { + loadTestFile(require.resolve('./legacy_and_multispace_monitor_api')); loadTestFile(require.resolve('./create_monitor_private_location')); loadTestFile(require.resolve('./create_monitor_project_private_location')); loadTestFile(require.resolve('./create_monitor_project')); @@ -32,5 +33,6 @@ export default function ({ loadTestFile }: DeploymentAgnosticFtrProviderContext) loadTestFile(require.resolve('./synthetics_enablement')); loadTestFile(require.resolve('./test_now_monitor')); loadTestFile(require.resolve('./edit_private_location')); + loadTestFile(require.resolve('./get_private_location_monitors')); }); } diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/synthetics/legacy_and_multispace_monitor_api.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/synthetics/legacy_and_multispace_monitor_api.ts new file mode 100644 index 000000000000..f9cd73374e78 --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/synthetics/legacy_and_multispace_monitor_api.ts @@ -0,0 +1,447 @@ +/* + * 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 expect from '@kbn/expect'; +import { v4 as uuidv4 } from 'uuid'; +import { RoleCredentials } from '@kbn/ftr-common-functional-services'; +import { SYNTHETICS_API_URLS } from '@kbn/synthetics-plugin/common/constants'; +import { + syntheticsMonitorSavedObjectType, + legacySyntheticsMonitorTypeSingle, +} from '@kbn/synthetics-plugin/common/types/saved_objects'; +import { DeploymentAgnosticFtrProviderContext } from '../../../ftr_provider_context'; +import { getFixtureJson } from './helpers/get_fixture_json'; + +export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { + describe('LegacyAndMultiSpaceMonitorAPI', function () { + const supertest = getService('supertestWithoutAuth'); + const kibanaServer = getService('kibanaServer'); + const samlAuth = getService('samlAuth'); + let editorUser: RoleCredentials; + + const saveMonitor = async (monitor: any, type: string, spaceId?: string) => { + let url = SYNTHETICS_API_URLS.SYNTHETICS_MONITORS + `?internal=true&savedObjectType=${type}`; + if (spaceId) { + url = `/s/${spaceId}${url}`; + monitor.spaces = [spaceId]; + } + const res = await supertest + .post(url) + .set(editorUser.apiKeyHeader) + .set(samlAuth.getInternalRequestHeader()) + .send(monitor); + expect(res.status).eql(200, JSON.stringify(res.body)); + return res.body; + }; + + const editMonitor = async (monitorId: string, monitor: any, type: string, spaceId?: string) => { + let url = SYNTHETICS_API_URLS.SYNTHETICS_MONITORS + `/${monitorId}?internal=true`; + if (spaceId) { + url = `/s/${spaceId}${url}`; + } + const res = await supertest + .put(url) + .set(editorUser.apiKeyHeader) + .set(samlAuth.getInternalRequestHeader()) + .send(monitor); + expect(res.status).eql(200, JSON.stringify(res.body)); + return res.body; + }; + + const deleteMonitor = async (monitorId: string, type: string, spaceId?: string) => { + let url = SYNTHETICS_API_URLS.SYNTHETICS_MONITORS + `/${monitorId}`; + if (spaceId) { + url = `/s/${spaceId}${url}`; + } + const res = await supertest + .delete(url) + .set(editorUser.apiKeyHeader) + .set(samlAuth.getInternalRequestHeader()) + .send(); + expect(res.status).eql(200, JSON.stringify(res.body)); + return res.body; + }; + + let legacyMonitor: any; + let multiMonitor: any; + let uuid: string; + let httpMonitor: any; + let editedLegacy: any; + let editedMulti: any; + let delLegacy: any; + let delMulti: any; + + before(async () => { + await kibanaServer.savedObjects.cleanStandardList(); + editorUser = await samlAuth.createM2mApiKeyWithRoleScope('editor'); + }); + + after(async () => { + await kibanaServer.savedObjects.cleanStandardList(); + }); + + beforeEach(() => { + uuid = uuidv4(); + httpMonitor = getFixtureJson('http_monitor'); + }); + + describe('Legacy and Multi-space monitor CRUD', () => { + it('should create a legacy monitor', async () => { + legacyMonitor = await saveMonitor( + { ...httpMonitor, name: `legacy-${uuid}` }, + legacySyntheticsMonitorTypeSingle + ); + expect(legacyMonitor.name).eql(`legacy-${uuid}`); + await kibanaServer.savedObjects + .find({ + type: legacySyntheticsMonitorTypeSingle, + }) + .then((response) => { + expect(response.saved_objects.length).to.eql(1); + expect(response.saved_objects[0].id).to.eql(legacyMonitor.id); + }); + }); + + it('should create a multi-space monitor', async () => { + multiMonitor = await saveMonitor( + { ...httpMonitor, name: `multi-${uuid}` }, + syntheticsMonitorSavedObjectType + ); + expect(multiMonitor.name).eql(`multi-${uuid}`); + const response = await kibanaServer.savedObjects.find({ + type: syntheticsMonitorSavedObjectType, + }); + const found = response.saved_objects.find((obj: any) => obj.id === multiMonitor.id); + expect(found).not.to.be(undefined); + expect(found?.attributes.name).to.eql(`multi-${uuid}`); + }); + + it('should edit a legacy monitor', async () => { + if (!legacyMonitor) { + legacyMonitor = await saveMonitor( + { ...httpMonitor, name: `legacy-${uuid}` }, + legacySyntheticsMonitorTypeSingle + ); + } + editedLegacy = await editMonitor( + legacyMonitor.id, + { name: `legacy-edited-${uuid}` }, + legacySyntheticsMonitorTypeSingle + ); + expect(editedLegacy.name).eql(`legacy-edited-${uuid}`); + const response = await kibanaServer.savedObjects.find({ + type: legacySyntheticsMonitorTypeSingle, + }); + const found = response.saved_objects.find((obj: any) => obj.id === legacyMonitor.id); + expect(found).not.to.be(undefined); + expect(found?.attributes.name).to.eql(`legacy-edited-${uuid}`); + }); + + it('should edit a multi-space monitor', async () => { + if (!multiMonitor) { + multiMonitor = await saveMonitor( + { ...httpMonitor, name: `multi-${uuid}` }, + syntheticsMonitorSavedObjectType + ); + } + editedMulti = await editMonitor( + multiMonitor.id, + { name: `multi-edited-${uuid}` }, + syntheticsMonitorSavedObjectType + ); + expect(editedMulti.name).eql(`multi-edited-${uuid}`); + const response = await kibanaServer.savedObjects.find({ + type: syntheticsMonitorSavedObjectType, + }); + const found = response.saved_objects.find((obj: any) => obj.id === multiMonitor.id); + expect(found).not.to.be(undefined); + expect(found?.attributes.name).to.eql(`multi-edited-${uuid}`); + }); + + it('should delete a legacy monitor', async () => { + if (!legacyMonitor) { + legacyMonitor = await saveMonitor( + { ...httpMonitor, name: `legacy-${uuid}` }, + legacySyntheticsMonitorTypeSingle + ); + } + delLegacy = await deleteMonitor(legacyMonitor.id, legacySyntheticsMonitorTypeSingle); + expect(delLegacy[0].id).eql(legacyMonitor.id); + const response = await kibanaServer.savedObjects.find({ + type: legacySyntheticsMonitorTypeSingle, + }); + const found = response.saved_objects.find((obj: any) => obj.id === legacyMonitor.id); + expect(found).to.be(undefined); + }); + + it('should delete a multi-space monitor', async () => { + if (!multiMonitor) { + multiMonitor = await saveMonitor( + { ...httpMonitor, name: `multi-${uuid}` }, + syntheticsMonitorSavedObjectType + ); + } + delMulti = await deleteMonitor(multiMonitor.id, syntheticsMonitorSavedObjectType); + expect(delMulti[0].id).eql(multiMonitor.id); + const response = await kibanaServer.savedObjects.find({ + type: syntheticsMonitorSavedObjectType, + }); + const found = response.saved_objects.find((obj: any) => obj.id === multiMonitor.id); + expect(found).to.be(undefined); + }); + + it('should allow editing spaces of a legacy monitor (convert to multi-space type)', async () => { + const legacy = await saveMonitor( + { ...httpMonitor, name: `legacy-to-multi-${uuid}` }, + legacySyntheticsMonitorTypeSingle + ); + const NEW_SPACE = `edit-space-${uuid}`; + await kibanaServer.spaces.create({ id: NEW_SPACE, name: `Edit Space ${uuid}` }); + + await editMonitor( + legacy.id, + { spaces: ['default', NEW_SPACE], name: `legacy-now-multi-${uuid}` }, + legacySyntheticsMonitorTypeSingle + ); + + const multiRes = await kibanaServer.savedObjects.find({ + type: syntheticsMonitorSavedObjectType, + }); + const foundMulti = multiRes.saved_objects.find((obj: any) => obj.id === legacy.id); + expect(foundMulti).not.to.be(undefined); + expect(foundMulti?.attributes.name).to.eql(`legacy-now-multi-${uuid}`); + expect(foundMulti?.namespaces?.includes(NEW_SPACE)).to.be(true); + + const legacyRes = await kibanaServer.savedObjects.find({ + type: legacySyntheticsMonitorTypeSingle, + }); + const foundLegacy = legacyRes.saved_objects.find((obj: any) => obj.id === legacy.id); + expect(foundLegacy).to.be(undefined); + + await deleteMonitor(legacy.id, syntheticsMonitorSavedObjectType); + }); + + it('should allow editing spaces of a multi-space monitor', async () => { + const multi = await saveMonitor( + { ...httpMonitor, name: `multi-edit-spaces-${uuid}` }, + syntheticsMonitorSavedObjectType + ); + const SPACE1 = `multi-space1-${uuid}`; + const SPACE2 = `multi-space2-${uuid}`; + await kibanaServer.spaces.create({ id: SPACE1, name: `Multi Space 1 ${uuid}` }); + await kibanaServer.spaces.create({ id: SPACE2, name: `Multi Space 2 ${uuid}` }); + + await editMonitor( + multi.id, + { spaces: ['default', SPACE1, SPACE2], name: `multi-edited-spaces-${uuid}` }, + syntheticsMonitorSavedObjectType + ); + + const multiRes = await kibanaServer.savedObjects.find({ + type: syntheticsMonitorSavedObjectType, + }); + const found = multiRes.saved_objects.find((obj: any) => obj.id === multi.id); + expect(found).not.to.be(undefined); + expect(found?.attributes.name).to.eql(`multi-edited-spaces-${uuid}`); + expect(found?.namespaces?.includes(SPACE1)).to.be(true); + expect(found?.namespaces?.includes(SPACE2)).to.be(true); + + await editMonitor( + multi.id, + { spaces: ['default', SPACE2], name: `multi-edited-spaces2-${uuid}` }, + syntheticsMonitorSavedObjectType + ); + const multiRes2 = await kibanaServer.savedObjects.find({ + type: syntheticsMonitorSavedObjectType, + }); + const found2 = multiRes2.saved_objects.find((obj: any) => obj.id === multi.id); + expect(found2?.namespaces?.includes(SPACE1)).to.be(false); + expect(found2?.namespaces?.includes(SPACE2)).to.be(true); + + await deleteMonitor(multi.id, syntheticsMonitorSavedObjectType); + }); + + it('should delete a monitor after editing spaces', async () => { + const legacy = await saveMonitor( + { ...httpMonitor, name: `legacy-del-after-edit-${uuid}` }, + legacySyntheticsMonitorTypeSingle + ); + const DEL_SPACE = `del-space-${uuid}`; + await kibanaServer.spaces.create({ id: DEL_SPACE, name: `Del Space ${uuid}` }); + await editMonitor( + legacy.id, + { spaces: ['default', DEL_SPACE], name: `legacy-del-multi-${uuid}` }, + legacySyntheticsMonitorTypeSingle + ); + const del = await deleteMonitor(legacy.id, syntheticsMonitorSavedObjectType); + expect(del[0].id).eql(legacy.id); + const multiRes = await kibanaServer.savedObjects.find({ + type: syntheticsMonitorSavedObjectType, + }); + const found = multiRes.saved_objects.find((obj: any) => obj.id === legacy.id); + expect(found).to.be(undefined); + }); + }); + + describe('Multi-space monitor filtering', () => { + let monitorDefault: any; + let monitorSpace: any; + let legacyMonitorSpace: any; + let SPACE_ID: string; + + beforeEach(async () => { + uuid = uuidv4(); + SPACE_ID = `test-space-${uuid}`; + await kibanaServer.spaces.create({ id: SPACE_ID, name: `Test Space ${uuid}` }); + monitorDefault = await saveMonitor( + { ...httpMonitor, name: `default-${uuid}` }, + syntheticsMonitorSavedObjectType + ); + monitorSpace = await saveMonitor( + { ...httpMonitor, name: `space-${uuid}` }, + syntheticsMonitorSavedObjectType, + SPACE_ID + ); + legacyMonitorSpace = await saveMonitor( + { ...httpMonitor, name: `legacy-space-${uuid}` }, + legacySyntheticsMonitorTypeSingle, + SPACE_ID + ); + }); + + it('should filter all monitors (showFromAllSpaces)', async () => { + const res = await supertest + .get(SYNTHETICS_API_URLS.SYNTHETICS_MONITORS + '?showFromAllSpaces=true&perPage=1000') + .set(editorUser.apiKeyHeader) + .set(samlAuth.getInternalRequestHeader()) + .expect(200); + const found = res.body.monitors.filter((m: any) => + [monitorDefault.id, monitorSpace.id, legacyMonitorSpace.id].includes(m.id) + ); + expect(found.length).eql(3); + + // Assert all monitors exist in their respective spaces + const defaultRes = await kibanaServer.savedObjects.find({ + type: syntheticsMonitorSavedObjectType, + }); + expect(defaultRes.saved_objects.some((obj: any) => obj.id === monitorDefault.id)).to.be( + true + ); + + const spaceRes = await kibanaServer.savedObjects.find({ + type: syntheticsMonitorSavedObjectType, + space: SPACE_ID, + }); + expect(spaceRes.saved_objects.some((obj: any) => obj.id === monitorSpace.id)).to.be(true); + + const legacySpaceRes = await kibanaServer.savedObjects.find({ + type: legacySyntheticsMonitorTypeSingle, + space: SPACE_ID, + }); + expect( + legacySpaceRes.saved_objects.some((obj: any) => obj.id === legacyMonitorSpace.id) + ).to.be(true); + }); + }); + + describe('Monitor search by name', () => { + let legacyMonitorSearch: any; + let multiMonitorSearch: any; + let searchUuid: string; + + beforeEach(async () => { + searchUuid = uuidv4(); + // Create a legacy monitor with a unique name + legacyMonitorSearch = await saveMonitor( + { ...httpMonitor, name: `legacy-search-${searchUuid}` }, + legacySyntheticsMonitorTypeSingle + ); + // Create a multi-space monitor with a unique name + multiMonitorSearch = await saveMonitor( + { ...httpMonitor, name: `multi-search-${searchUuid}` }, + syntheticsMonitorSavedObjectType + ); + }); + + it('should find both legacy and multi-space monitors by name', async () => { + const searchName = `search-${searchUuid}`; + const res = await supertest + .get( + SYNTHETICS_API_URLS.SYNTHETICS_MONITORS + + `?query=${encodeURIComponent(searchName)}&perPage=1000` + ) + .set(editorUser.apiKeyHeader) + .set(samlAuth.getInternalRequestHeader()); + + expect(res.status).eql(200, JSON.stringify(res.body)); + + // Should find both monitors by partial name match + const foundLegacy = res.body.monitors.find( + (m: any) => m.id === legacyMonitorSearch.id && m.name === `legacy-search-${searchUuid}` + ); + const foundMulti = res.body.monitors.find( + (m: any) => m.id === multiMonitorSearch.id && m.name === `multi-search-${searchUuid}` + ); + expect(foundLegacy).not.to.be(undefined); + expect(foundMulti).not.to.be(undefined); + }); + }); + + describe('Monitor space validation', () => { + it('should throw error if spaces list does not include the calling space on create', async () => { + const INVALID_SPACE = `invalid-space-${uuidv4()}`; + await kibanaServer.spaces.create({ id: INVALID_SPACE, name: `Invalid Space` }); + const monitorData = { + ...getFixtureJson('http_monitor'), + name: `invalid-create-${uuidv4()}`, + spaces: ['default'], + }; + const resp = await supertest + .post(`/s/${INVALID_SPACE}${SYNTHETICS_API_URLS.SYNTHETICS_MONITORS}?internal=true`) + .set(editorUser.apiKeyHeader) + .set(samlAuth.getInternalRequestHeader()) + .send(monitorData); + + expect(resp.status).to.be(400); + expect(resp.body.message).to.eql( + 'Invalid space ID provided in monitor configuration. It should always include the current space ID.' + ); + }); + + it('should throw error if spaces list does not include the calling space on edit', async () => { + const EDIT_SPACE = `edit-space-${uuidv4()}`; + await kibanaServer.spaces.create({ id: EDIT_SPACE, name: `Edit Space` }); + // Create monitor in EDIT_SPACE + const res = await supertest + .post( + `/s/${EDIT_SPACE}${SYNTHETICS_API_URLS.SYNTHETICS_MONITORS}?internal=true&savedObjectType=${syntheticsMonitorSavedObjectType}` + ) + .set(editorUser.apiKeyHeader) + .set(samlAuth.getInternalRequestHeader()) + .send({ + ...getFixtureJson('http_monitor'), + name: `edit-invalid-${uuidv4()}`, + spaces: [EDIT_SPACE], + }); + expect(res.status).eql(200, JSON.stringify(res.body)); + // Try to edit monitor with spaces not including EDIT_SPACE + const resp = await supertest + .put( + `/s/${EDIT_SPACE}${SYNTHETICS_API_URLS.SYNTHETICS_MONITORS}/${res.body.id}?internal=true` + ) + .set(editorUser.apiKeyHeader) + .set(samlAuth.getInternalRequestHeader()) + .send({ name: `edit-invalid-${uuidv4()}`, spaces: ['default'] }); + + expect(resp.status).to.be(400); + expect(resp.body.message).to.eql( + 'Invalid space ID provided in monitor configuration. It should always include the current space ID.' + ); + }); + }); + }); +} diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/synthetics/suggestions.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/synthetics/suggestions.ts index d6a42b6cc897..8a35277dcc1f 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/synthetics/suggestions.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/synthetics/suggestions.ts @@ -13,8 +13,9 @@ import { ProjectMonitorsRequest, PrivateLocation, } from '@kbn/synthetics-plugin/common/runtime_types'; -import { syntheticsMonitorType } from '@kbn/synthetics-plugin/common/types/saved_objects'; +import { syntheticsMonitorSavedObjectType } from '@kbn/synthetics-plugin/common/types/saved_objects'; import { SYNTHETICS_API_URLS } from '@kbn/synthetics-plugin/common/constants'; +import pMap from 'p-map'; import { DeploymentAgnosticFtrProviderContext } from '../../../ftr_provider_context'; import { getFixtureJson } from './helpers/get_fixture_json'; import { PrivateLocationTestService } from '../../../services/synthetics_private_location'; @@ -60,7 +61,7 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { before(async () => { await kibanaServer.savedObjects.clean({ types: [ - syntheticsMonitorType, + syntheticsMonitorSavedObjectType, 'ingest-agent-policies', 'ingest-package-policies', 'synthetics-private-location', @@ -91,7 +92,7 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { beforeEach(async () => { await kibanaServer.savedObjects.clean({ - types: [syntheticsMonitorType, 'ingest-package-policies'], + types: [syntheticsMonitorSavedObjectType, 'ingest-package-policies'], }); monitors = []; @@ -100,6 +101,7 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { ..._monitors[0], locations: [privateLocation], name: `${_monitors[0].name} ${i}`, + spaces: [], }); } }); @@ -121,54 +123,61 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { .set(samlAuth.getInternalRequestHeader()) .send(projectMonitors) .expect(200); - for (let i = 0; i < monitors.length; i++) { - await saveMonitor(monitors[i]); - } + await pMap( + monitors, + async (monitor) => { + return saveMonitor(monitor); + }, + { concurrency: 1 } + ); + const apiResponse = await supertest .get(`/s/${SPACE_ID}${SYNTHETICS_API_URLS.SUGGESTIONS}`) .set(editorUser.apiKeyHeader) .set(samlAuth.getInternalRequestHeader()) .expect(200); - expect(apiResponse.body).toEqual({ - locations: [ - { - count: 22, - label: privateLocation.label, - value: privateLocation.id, - }, - ], - monitorIds: expect.arrayContaining([ - ...monitors.map((monitor) => ({ - count: 1, - label: monitor.name, - value: expect.any(String), - })), - ...projectMonitors.monitors.slice(0, 2).map((monitor) => ({ - count: 1, - label: monitor.name, - value: expect.any(String), - })), - ]), - monitorTypes: [ - { - count: 20, - label: 'http', - value: 'http', - }, - { - count: 2, - label: 'icmp', - value: 'icmp', - }, - ], - projects: [ - { - count: 2, - label: project, - value: project, - }, - ], - tags: expect.arrayContaining([ + + expect(apiResponse.body.locations).toEqual([ + { + count: 22, + label: privateLocation.label, + value: privateLocation.id, + }, + ]); + const expectedIds = [ + ...monitors.map((monitor) => ({ + count: 1, + label: monitor.name, + value: expect.any(String), + })), + ...projectMonitors.monitors.slice(0, 2).map((monitor) => ({ + count: 1, + label: monitor.name, + value: expect.any(String), + })), + ]; + expect(apiResponse.body.monitorIds).toEqual(expect.arrayContaining(expectedIds)); + expect(apiResponse.body.monitorTypes).toEqual([ + { + count: 20, + label: 'http', + value: 'http', + }, + { + count: 2, + label: 'icmp', + value: 'icmp', + }, + ]); + expect(apiResponse.body.projects).toEqual([ + { + count: 2, + label: project, + value: project, + }, + ]); + expect(apiResponse.body.tags).toEqual( + expect.arrayContaining([ { count: 21, label: 'tag1', @@ -189,8 +198,8 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { label: 'service:smtp', value: 'service:smtp', }, - ]), - }); + ]) + ); }); it('handles query params for projects', async () => { diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/synthetics/test_now_monitor.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/synthetics/test_now_monitor.ts index 1efe174a2c66..9ea61196b831 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/synthetics/test_now_monitor.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/synthetics/test_now_monitor.ts @@ -75,8 +75,9 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { .post(`/s/${SPACE_ID}${SYNTHETICS_API_URLS.SYNTHETICS_MONITORS}`) .set(editorUser.apiKeyHeader) .set(samlAuth.getInternalRequestHeader()) - .send(newMonitor) - .expect(200); + .send({ ...newMonitor, spaces: [] }); + + expect(resp.status).to.eql(200, JSON.stringify(resp.body)); const res = await supertest .post(`/s/${SPACE_ID}${SYNTHETICS_API_URLS.TRIGGER_MONITOR}/${resp.body.id}`) @@ -91,7 +92,7 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { expect(result.locations).to.eql([LOCAL_LOCATION]); expect(omit(result.monitor, ['id', 'config_id'])).to.eql( - omit(newMonitor, ['id', 'config_id']) + omit({ ...newMonitor, spaces: [SPACE_ID] }, ['id', 'config_id']) ); }); }); diff --git a/x-pack/test/api_integration/deployment_agnostic/services/synthetics_monitor.ts b/x-pack/test/api_integration/deployment_agnostic/services/synthetics_monitor.ts index e2bd2881db95..a4a2d6ed6081 100644 --- a/x-pack/test/api_integration/deployment_agnostic/services/synthetics_monitor.ts +++ b/x-pack/test/api_integration/deployment_agnostic/services/synthetics_monitor.ts @@ -6,7 +6,7 @@ */ import { RoleCredentials, SamlAuthProviderType } from '@kbn/ftr-common-functional-services'; import { SYNTHETICS_API_URLS } from '@kbn/synthetics-plugin/common/constants'; -import { syntheticsMonitorType } from '@kbn/synthetics-plugin/common/types/saved_objects'; +import { syntheticsMonitorSavedObjectType } from '@kbn/synthetics-plugin/common/types/saved_objects'; import { EncryptedSyntheticsSavedMonitor } from '@kbn/synthetics-plugin/common/runtime_types'; import { MonitorInspectResponse } from '@kbn/synthetics-plugin/public/apps/synthetics/state/monitor_management/api'; import { v4 as uuidv4 } from 'uuid'; @@ -166,7 +166,7 @@ export class SyntheticsMonitorTestService { const response = await this.supertest .get(`/s/${space}${SYNTHETICS_API_URLS.SYNTHETICS_MONITORS}`) .query({ - filter: `${syntheticsMonitorType}.attributes.journey_id: "${journeyId}" AND ${syntheticsMonitorType}.attributes.project_id: "${projectId}"`, + filter: `${syntheticsMonitorSavedObjectType}.attributes.journey_id: "${journeyId}" AND ${syntheticsMonitorSavedObjectType}.attributes.project_id: "${projectId}"`, }) .set(user.apiKeyHeader) .set(this.samlAuth.getInternalRequestHeader()) diff --git a/x-pack/test_serverless/api_integration/test_suites/observability/platform_security/authorization.ts b/x-pack/test_serverless/api_integration/test_suites/observability/platform_security/authorization.ts index e08bf09808e9..19a5c459750d 100644 --- a/x-pack/test_serverless/api_integration/test_suites/observability/platform_security/authorization.ts +++ b/x-pack/test_serverless/api_integration/test_suites/observability/platform_security/authorization.ts @@ -7400,6 +7400,18 @@ export default function ({ getService }: FtrProviderContext) { "saved_object:synthetics-monitor/delete", "saved_object:synthetics-monitor/bulk_delete", "saved_object:synthetics-monitor/share_to_space", + "saved_object:synthetics-monitor-multi-space/bulk_get", + "saved_object:synthetics-monitor-multi-space/get", + "saved_object:synthetics-monitor-multi-space/find", + "saved_object:synthetics-monitor-multi-space/open_point_in_time", + "saved_object:synthetics-monitor-multi-space/close_point_in_time", + "saved_object:synthetics-monitor-multi-space/create", + "saved_object:synthetics-monitor-multi-space/bulk_create", + "saved_object:synthetics-monitor-multi-space/update", + "saved_object:synthetics-monitor-multi-space/bulk_update", + "saved_object:synthetics-monitor-multi-space/delete", + "saved_object:synthetics-monitor-multi-space/bulk_delete", + "saved_object:synthetics-monitor-multi-space/share_to_space", "saved_object:uptime-synthetics-api-key/bulk_get", "saved_object:uptime-synthetics-api-key/get", "saved_object:uptime-synthetics-api-key/find", @@ -8223,6 +8235,18 @@ export default function ({ getService }: FtrProviderContext) { "saved_object:synthetics-monitor/delete", "saved_object:synthetics-monitor/bulk_delete", "saved_object:synthetics-monitor/share_to_space", + "saved_object:synthetics-monitor-multi-space/bulk_get", + "saved_object:synthetics-monitor-multi-space/get", + "saved_object:synthetics-monitor-multi-space/find", + "saved_object:synthetics-monitor-multi-space/open_point_in_time", + "saved_object:synthetics-monitor-multi-space/close_point_in_time", + "saved_object:synthetics-monitor-multi-space/create", + "saved_object:synthetics-monitor-multi-space/bulk_create", + "saved_object:synthetics-monitor-multi-space/update", + "saved_object:synthetics-monitor-multi-space/bulk_update", + "saved_object:synthetics-monitor-multi-space/delete", + "saved_object:synthetics-monitor-multi-space/bulk_delete", + "saved_object:synthetics-monitor-multi-space/share_to_space", "saved_object:uptime-synthetics-api-key/bulk_get", "saved_object:uptime-synthetics-api-key/get", "saved_object:uptime-synthetics-api-key/find", @@ -8982,6 +9006,11 @@ export default function ({ getService }: FtrProviderContext) { "saved_object:synthetics-dynamic-settings/find", "saved_object:synthetics-dynamic-settings/open_point_in_time", "saved_object:synthetics-dynamic-settings/close_point_in_time", + "saved_object:synthetics-monitor-multi-space/bulk_get", + "saved_object:synthetics-monitor-multi-space/get", + "saved_object:synthetics-monitor-multi-space/find", + "saved_object:synthetics-monitor-multi-space/open_point_in_time", + "saved_object:synthetics-monitor-multi-space/close_point_in_time", "saved_object:synthetics-monitor/bulk_get", "saved_object:synthetics-monitor/get", "saved_object:synthetics-monitor/find", @@ -9333,6 +9362,11 @@ export default function ({ getService }: FtrProviderContext) { "saved_object:synthetics-dynamic-settings/find", "saved_object:synthetics-dynamic-settings/open_point_in_time", "saved_object:synthetics-dynamic-settings/close_point_in_time", + "saved_object:synthetics-monitor-multi-space/bulk_get", + "saved_object:synthetics-monitor-multi-space/get", + "saved_object:synthetics-monitor-multi-space/find", + "saved_object:synthetics-monitor-multi-space/open_point_in_time", + "saved_object:synthetics-monitor-multi-space/close_point_in_time", "saved_object:synthetics-monitor/bulk_get", "saved_object:synthetics-monitor/get", "saved_object:synthetics-monitor/find",