From f317cec25b3cdfcc7063ff21a4b23e2f9e5f876e Mon Sep 17 00:00:00 2001 From: Shahzad Date: Wed, 25 Jun 2025 10:47:47 +0200 Subject: [PATCH] [Synthetics] Multi space monitors !! (#221568) ## Summary Multi space monitors !! Fixes https://github.com/elastic/kibana/issues/164294 User will be able to choose in which space monitors will be available !! image ### Technical This is being done by registering another saved object type and for existing monitors it will continue to work as right now but for newly created monitors user will have ability to specify spaces or choose multiple spaces or all. ### Testing 1. Create few monitors before this PR in multiple spaces 2. Create multiple monitors in multiple spaces after this PR 3. Make sure filtering, editing and deleting, creating works as expected on both set of monitors --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .../current_fields.json | 28 ++ .../current_mappings.json | 130 +++++ .../server-internal/src/object_types/index.ts | 2 +- .../check_registered_types.test.ts | 1 + .../registration/type_registrations.test.ts | 1 + .../kbn_client/kbn_client_saved_objects.ts | 1 + .../translations/translations/fr-FR.json | 1 - .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - .../public/components/tags_list/tags_list.tsx | 2 +- .../common/constants/monitor_defaults.ts | 1 + .../common/constants/monitor_management.ts | 1 + .../monitor_management/monitor_types.ts | 3 +- .../synthetics_overview_status.ts | 2 +- .../synthetics/common/types/saved_objects.ts | 12 +- .../synthetics/common/utils/date_util.ts | 64 +++ .../project_monitor_read_only.journey.ts | 12 +- .../journeys/services/add_monitor.ts | 4 +- .../journeys/services/synthetics_services.ts | 4 +- .../fields/monitor_spaces.test.ts | 56 +++ .../fields/monitor_spaces.tsx | 135 ++++++ .../monitor_add_edit/form/field_config.tsx | 22 + .../monitor_add_edit/form/field_wrappers.tsx | 5 + .../monitor_add_edit/form/form_config.tsx | 16 + .../monitor_add_edit/form/index.tsx | 40 +- .../monitor_edit_page.test.tsx | 5 + .../components/monitor_add_edit/types.ts | 1 + .../management/monitor_list_table/columns.tsx | 27 +- .../monitor_list_table/monitor_list.tsx | 17 +- .../overview/overview/actions_popover.tsx | 4 +- .../components/monitor_status_col.tsx | 69 +++ .../components/monitors_table.tsx | 4 +- .../hooks/use_monitors_table_columns.tsx | 71 ++- .../overview/metric_item/metric_item.tsx | 4 +- .../overview/monitor_detail_flyout.tsx | 15 +- .../overview/overview/overview_grid.tsx | 4 +- .../monitors_page/overview/overview/types.ts | 2 +- .../settings/components/spaces_select.tsx | 2 +- .../contexts/synthetics_shared_context.tsx | 14 +- .../hooks/use_edit_monitor_locator.ts | 17 +- .../hooks/use_monitor_detail_locator.ts | 9 +- .../public/utils/api_service/api_service.ts | 2 +- .../status_rule/queries/filter_monitors.ts | 5 +- .../status_rule/queries/helpers.ts | 9 +- .../queries/query_monitor_status_alert.ts | 2 +- .../status_rule/status_rule_executor.test.ts | 2 +- .../status_rule/status_rule_executor.ts | 6 +- .../tls_rule/tls_rule_executor.test.ts | 8 +- .../alert_rules/tls_rule/tls_rule_executor.ts | 12 +- .../plugins/synthetics/server/feature.ts | 13 +- .../server/mocks/route_context_mock.ts | 38 ++ .../synthetics/server/mocks/server_mock.ts | 51 ++ .../routes/certs/get_certificates.test.ts | 2 +- .../server/routes/certs/get_certificates.ts | 6 +- .../synthetics/server/routes/common.test.ts | 12 +- .../synthetics/server/routes/common.ts | 59 +-- .../default_alerts/default_alert_service.ts | 6 +- .../default_alerts/update_default_alert.ts | 14 +- .../server/routes/filters/filters.ts | 18 +- .../plugins/synthetics/server/routes/index.ts | 2 +- .../routes/monitor_cruds/add_monitor.ts | 31 +- .../add_monitor/add_monitor_api.test.ts | 58 +++ .../add_monitor/add_monitor_api.ts | 26 +- .../bulk_cruds/add_monitor_bulk.ts | 4 +- .../bulk_cruds/edit_monitor_bulk.ts | 3 + .../routes/monitor_cruds/edit_monitor.test.ts | 58 +-- .../routes/monitor_cruds/edit_monitor.ts | 36 +- .../routes/monitor_cruds/get_monitor.ts | 17 +- .../routes/monitor_cruds/get_monitors_list.ts | 28 +- .../routes/monitor_cruds/inspect_monitor.ts | 2 +- .../monitor_cruds/monitor_validation.test.ts | 95 ++-- .../monitor_cruds/monitor_validation.ts | 28 +- .../project_monitor/add_monitor_project.ts | 18 + .../project_monitor/delete_monitor_project.ts | 26 +- .../project_monitor/get_monitor_project.ts | 34 +- .../services/delete_monitor_api.ts | 22 +- .../overview_status_service.test.ts | 67 ++- .../overview_status_service.ts | 7 +- .../routes/settings/dynamic_settings.ts | 14 +- .../delete_private_location.ts | 24 +- .../get_location_monitors.ts | 90 ++-- .../private_locations/helpers.test.ts | 2 + .../settings/private_locations/helpers.ts | 10 +- .../server/routes/suggestions/route.ts | 166 ------- .../routes/suggestions/suggestions_route.ts | 261 ++++++++++ .../synthetics_service/run_once_monitor.ts | 7 +- .../synthetics_service/test_now_monitor.ts | 15 +- .../synthetics/server/saved_objects/index.ts | 5 +- .../migrations/monitors/8.6.0.ts | 2 +- .../migrations/monitors/8.8.0.test.ts | 1 + .../migrations/monitors/8.8.0.ts | 17 +- .../migrations/monitors/8.9.0.ts | 6 +- .../server/saved_objects/saved_objects.ts | 94 +--- .../saved_objects/synthetics_monitor.ts | 297 ------------ .../saved_objects/synthetics_monitor/index.ts | 13 + .../legacy_synthetics_monitor.ts | 109 +++++ .../synthetics_monitor/monitor_mappings.ts | 139 ++++++ ...itors.test.ts => process_monitors.test.ts} | 2 +- ...et_all_monitors.ts => process_monitors.ts} | 0 .../synthetics_monitor_config.ts | 93 ++++ .../server/saved_objects/synthetics_param.ts | 2 +- .../saved_objects/synthetics_settings.ts | 51 +- .../monitor_config_repository.test.ts | 260 +++++++--- .../services/monitor_config_repository.ts | 273 ++++++++--- .../private_formatters/common_formatters.ts | 1 + .../processors_formatter.ts | 4 +- .../formatters/public_formatters/common.ts | 1 + .../public_formatters/format_configs.ts | 5 +- .../synthetics_private_location.test.ts | 3 +- .../synthetics_private_location.ts | 3 +- .../project_monitor_formatter.test.ts | 4 +- .../project_monitor_formatter.ts | 6 +- .../synthetics_monitor_client.ts | 4 +- .../synthetics_service/synthetics_service.ts | 8 +- .../apis/synthetics/add_monitor.ts | 4 +- .../add_monitor_private_location.ts | 1 + .../apis/synthetics/add_monitor_project.ts | 18 +- .../synthetics_monitor_test_service.ts | 4 +- .../synthetics/create_monitor.ts | 20 +- .../create_monitor_private_location.ts | 6 +- .../synthetics/create_monitor_project.ts | 199 +++++++- ...create_monitor_project_private_location.ts | 54 ++- .../synthetics/create_monitor_public_api.ts | 7 + ...ate_monitor_public_api_private_location.ts | 7 + .../synthetics/delete_monitor_project.ts | 28 +- .../edit_monitor_private_location.ts | 5 +- .../synthetics/edit_monitor_public_api.ts | 5 + ...dit_monitor_public_api_private_location.ts | 5 + .../synthetics/enable_default_alerting.ts | 10 +- .../synthetics/fixtures/http_monitor.json | 3 +- .../observability/synthetics/get_filters.ts | 6 +- .../observability/synthetics/get_monitor.ts | 9 +- .../get_private_location_monitors.ts | 126 +++++ .../apis/observability/synthetics/index.ts | 2 + .../legacy_and_multispace_monitor_api.ts | 447 ++++++++++++++++++ .../observability/synthetics/suggestions.ts | 105 ++-- .../synthetics/test_now_monitor.ts | 7 +- .../services/synthetics_monitor.ts | 4 +- .../platform_security/authorization.ts | 34 ++ 139 files changed, 3470 insertions(+), 1275 deletions(-) create mode 100644 x-pack/solutions/observability/plugins/synthetics/common/utils/date_util.ts create mode 100644 x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/fields/monitor_spaces.test.ts create mode 100644 x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/fields/monitor_spaces.tsx create mode 100644 x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/compact_view/components/monitor_status_col.tsx create mode 100644 x-pack/solutions/observability/plugins/synthetics/server/mocks/route_context_mock.ts create mode 100644 x-pack/solutions/observability/plugins/synthetics/server/mocks/server_mock.ts delete mode 100644 x-pack/solutions/observability/plugins/synthetics/server/routes/suggestions/route.ts create mode 100644 x-pack/solutions/observability/plugins/synthetics/server/routes/suggestions/suggestions_route.ts create mode 100644 x-pack/solutions/observability/plugins/synthetics/server/saved_objects/synthetics_monitor/index.ts create mode 100644 x-pack/solutions/observability/plugins/synthetics/server/saved_objects/synthetics_monitor/legacy_synthetics_monitor.ts create mode 100644 x-pack/solutions/observability/plugins/synthetics/server/saved_objects/synthetics_monitor/monitor_mappings.ts rename x-pack/solutions/observability/plugins/synthetics/server/saved_objects/synthetics_monitor/{get_all_monitors.test.ts => process_monitors.test.ts} (99%) rename x-pack/solutions/observability/plugins/synthetics/server/saved_objects/synthetics_monitor/{get_all_monitors.ts => process_monitors.ts} (100%) create mode 100644 x-pack/solutions/observability/plugins/synthetics/server/saved_objects/synthetics_monitor/synthetics_monitor_config.ts create mode 100644 x-pack/test/api_integration/deployment_agnostic/apis/observability/synthetics/get_private_location_monitors.ts create mode 100644 x-pack/test/api_integration/deployment_agnostic/apis/observability/synthetics/legacy_and_multispace_monitor_api.ts 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",