diff --git a/x-pack/solutions/observability/plugins/synthetics/e2e/synthetics/journeys/alert_rules/custom_status_alert.journey.ts b/x-pack/solutions/observability/plugins/synthetics/e2e/synthetics/journeys/alert_rules/custom_status_alert.journey.ts index a5e6361d2eb4..8dd2ed593db5 100644 --- a/x-pack/solutions/observability/plugins/synthetics/e2e/synthetics/journeys/alert_rules/custom_status_alert.journey.ts +++ b/x-pack/solutions/observability/plugins/synthetics/e2e/synthetics/journeys/alert_rules/custom_status_alert.journey.ts @@ -19,11 +19,11 @@ journey(`CustomStatusAlert`, async ({ page, params }) => { let configId: string; before(async () => { - await services.cleaUp(); + await services.cleanUp(); }); after(async () => { - await services.cleaUp(); + await services.cleanUp(); }); step('Go to monitors page', async () => { diff --git a/x-pack/solutions/observability/plugins/synthetics/e2e/synthetics/journeys/alert_rules/default_status_alert.journey.ts b/x-pack/solutions/observability/plugins/synthetics/e2e/synthetics/journeys/alert_rules/default_status_alert.journey.ts index e2285d499a0f..1aceee301312 100644 --- a/x-pack/solutions/observability/plugins/synthetics/e2e/synthetics/journeys/alert_rules/default_status_alert.journey.ts +++ b/x-pack/solutions/observability/plugins/synthetics/e2e/synthetics/journeys/alert_rules/default_status_alert.journey.ts @@ -28,11 +28,11 @@ journey(`DefaultStatusAlert`, async ({ page, params }) => { let configId2: string; before(async () => { - await services.cleaUp(); + await services.cleanUp(); }); after(async () => { - await services.cleaUp(); + await services.cleanUp(); }); step('setup monitor', async () => { diff --git a/x-pack/solutions/observability/plugins/synthetics/e2e/synthetics/journeys/monitor_details_page/monitor_summary.journey.ts b/x-pack/solutions/observability/plugins/synthetics/e2e/synthetics/journeys/monitor_details_page/monitor_summary.journey.ts index 00d637ebedb8..630eb9ae8a8b 100644 --- a/x-pack/solutions/observability/plugins/synthetics/e2e/synthetics/journeys/monitor_details_page/monitor_summary.journey.ts +++ b/x-pack/solutions/observability/plugins/synthetics/e2e/synthetics/journeys/monitor_details_page/monitor_summary.journey.ts @@ -30,7 +30,7 @@ journeySkip(`MonitorSummaryTab`, async ({ page, params }) => { let configId: string; before(async () => { - await services.cleaUp(); + await services.cleanUp(); await services.enableMonitorManagedViaApi(); configId = await services.addTestMonitor('Test Monitor', { type: 'http', @@ -44,7 +44,7 @@ journeySkip(`MonitorSummaryTab`, async ({ page, params }) => { }); after(async () => { - await services.cleaUp(); + await services.cleanUp(); }); step('Go to monitor summary page', async () => { diff --git a/x-pack/solutions/observability/plugins/synthetics/e2e/synthetics/journeys/overview_save_lens_visualization.journey.ts b/x-pack/solutions/observability/plugins/synthetics/e2e/synthetics/journeys/overview_save_lens_visualization.journey.ts index a380f1c41bbe..d4f87466f6e5 100644 --- a/x-pack/solutions/observability/plugins/synthetics/e2e/synthetics/journeys/overview_save_lens_visualization.journey.ts +++ b/x-pack/solutions/observability/plugins/synthetics/e2e/synthetics/journeys/overview_save_lens_visualization.journey.ts @@ -14,11 +14,11 @@ journey('OverviewSaveLensVisualization', async ({ page, params }) => { const syntheticsService = new SyntheticsServices(params); before(async () => { - await syntheticsService.cleaUp(); + await syntheticsService.cleanUp(); }); after(async () => { - await syntheticsService.cleaUp(); + await syntheticsService.cleanUp(); }); step('Go to Monitors overview page', async () => { 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 76f45466eb04..476ce2382398 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 @@ -190,20 +190,20 @@ export class SyntheticsServices { }); } - async cleaUp() { + async cleanUp() { try { const getService = this.params.getService; const server = getService('kibanaServer'); await server.savedObjects.clean({ types: ['synthetics-monitor', 'alert'] }); - await this.cleaUpAlerts(); + await this.cleanUpAlerts(); } catch (e) { // eslint-disable-next-line no-console console.log(e); } } - async cleaUpAlerts() { + async cleanUpAlerts() { try { const getService = this.params.getService; const es: Client = getService('es'); diff --git a/x-pack/solutions/observability/plugins/synthetics/e2e/synthetics/journeys/step_details.journey.ts b/x-pack/solutions/observability/plugins/synthetics/e2e/synthetics/journeys/step_details.journey.ts index 4fadd4d5fd6e..9ede7aff6b47 100644 --- a/x-pack/solutions/observability/plugins/synthetics/e2e/synthetics/journeys/step_details.journey.ts +++ b/x-pack/solutions/observability/plugins/synthetics/e2e/synthetics/journeys/step_details.journey.ts @@ -15,7 +15,7 @@ journey(`StepDetailsPage`, async ({ page, params }) => { const services = new SyntheticsServices(params); before(async () => { - await services.cleaUp(); + await services.cleanUp(); await services.enableMonitorManagedViaApi(); await services.addTestMonitor( 'https://www.google.com', @@ -33,7 +33,7 @@ journey(`StepDetailsPage`, async ({ page, params }) => { }); after(async () => { - await services.cleaUp(); + await services.cleanUp(); }); step('Go to step details page', async () => { diff --git a/x-pack/solutions/observability/plugins/synthetics/e2e/synthetics/journeys/test_now_mode.journey.ts b/x-pack/solutions/observability/plugins/synthetics/e2e/synthetics/journeys/test_now_mode.journey.ts index 44f14b6031f7..100b54ca9568 100644 --- a/x-pack/solutions/observability/plugins/synthetics/e2e/synthetics/journeys/test_now_mode.journey.ts +++ b/x-pack/solutions/observability/plugins/synthetics/e2e/synthetics/journeys/test_now_mode.journey.ts @@ -53,7 +53,7 @@ journeySkip(`TestNowMode`, async ({ page, params }) => { } }); - await services.cleaUp(); + await services.cleanUp(); await services.enableMonitorManagedViaApi(); await services.addTestMonitor('Test Monitor', { type: 'http', @@ -65,7 +65,7 @@ journeySkip(`TestNowMode`, async ({ page, params }) => { }); after(async () => { - await services.cleaUp(); + await services.cleanUp(); }); step('Go to monitors page', async () => { diff --git a/x-pack/solutions/observability/plugins/synthetics/e2e/synthetics/journeys/test_run_details.journey.ts b/x-pack/solutions/observability/plugins/synthetics/e2e/synthetics/journeys/test_run_details.journey.ts index e4054f243584..cc6e2ed02079 100644 --- a/x-pack/solutions/observability/plugins/synthetics/e2e/synthetics/journeys/test_run_details.journey.ts +++ b/x-pack/solutions/observability/plugins/synthetics/e2e/synthetics/journeys/test_run_details.journey.ts @@ -21,7 +21,7 @@ journeySkip(`TestRunDetailsPage`, async ({ page, params }) => { const services = new SyntheticsServices(params); before(async () => { - await services.cleaUp(); + await services.cleanUp(); await services.enableMonitorManagedViaApi(); await services.addTestMonitor( 'https://www.google.com', @@ -47,7 +47,7 @@ journeySkip(`TestRunDetailsPage`, async ({ page, params }) => { }); after(async () => { - await services.cleaUp(); + await services.cleanUp(); }); step('Go to monitor summary page', async () => { 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 e385f57c65df..e04679692826 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 @@ -12,7 +12,6 @@ import { mockEncryptedSO } from '../../synthetics_service/utils/mocks'; import { elasticsearchClientMock } from '@kbn/core-elasticsearch-client-server-mocks'; import { SyntheticsMonitorClient } from '../../synthetics_service/synthetics_monitor/synthetics_monitor_client'; import { SyntheticsService } from '../../synthetics_service/synthetics_service'; -import * as monitorUtils from '../../saved_objects/synthetics_monitor/get_all_monitors'; import * as locationsUtils from '../../synthetics_service/get_all_locations'; import type { PublicLocation } from '../../../common/runtime_types'; import { SyntheticsServerSetup } from '../../types'; @@ -82,10 +81,11 @@ describe('StatusRuleExecutor', () => { name: 'test', }, } as any); + const configRepo = statusRule.monitorConfigRepository; describe('DefaultRule', () => { it('should only query enabled monitors', async () => { - const spy = jest.spyOn(monitorUtils, 'getAllMonitors').mockResolvedValue([]); + const spy = jest.spyOn(configRepo, 'getAll').mockResolvedValue([]); const { downConfigs, staleDownConfigs } = await statusRule.getDownChecks({}); @@ -94,7 +94,6 @@ describe('StatusRuleExecutor', () => { expect(spy).toHaveBeenCalledWith({ filter: 'synthetics-monitor.attributes.alert.status.enabled: true', - soClient, }); }); @@ -129,8 +128,10 @@ describe('StatusRuleExecutor', () => { } as any ); - // Mock the getAllMonitors function to return test monitors with a location - jest.spyOn(monitorUtils, 'getAllMonitors').mockResolvedValue(testMonitors); + // Mock the getAll method to return test monitors with a location + jest + .spyOn(statusRuleWithEmptyLocations.monitorConfigRepository, 'getAll') + .mockResolvedValue(testMonitors); // Execute await statusRuleWithEmptyLocations.getDownChecks({}); @@ -144,7 +145,7 @@ describe('StatusRuleExecutor', () => { }); it('marks deleted configs as expected', async () => { - jest.spyOn(monitorUtils, 'getAllMonitors').mockResolvedValue(testMonitors); + jest.spyOn(configRepo, 'getAll').mockResolvedValue(testMonitors); const { downConfigs } = await statusRule.getDownChecks({}); @@ -220,7 +221,7 @@ describe('StatusRuleExecutor', () => { }); it('does not mark deleted config when monitor does not contain location label', async () => { - jest.spyOn(monitorUtils, 'getAllMonitors').mockResolvedValue([ + jest.spyOn(configRepo, 'getAll').mockResolvedValue([ { ...testMonitors[0], attributes: { 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 7d9dbe7c034c..20ce28295487 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, uniq } from 'lodash'; import { getAlertDetailsUrl } from '@kbn/observability-plugin/common'; import { SyntheticsMonitorStatusRuleParams as StatusRuleParams } from '@kbn/response-ops-rule-params/synthetics_monitor_status'; +import { MonitorConfigRepository } from '../../services/monitor_config_repository'; import { AlertOverviewStatus, AlertStatusConfigs, @@ -38,10 +39,7 @@ import { queryMonitorStatusAlert } from './queries/query_monitor_status_alert'; import { parseArrayFilters, parseLocationFilter } from '../../routes/common'; import { SyntheticsServerSetup } from '../../types'; import { SyntheticsEsClient } from '../../lib'; -import { - getAllMonitors, - processMonitors, -} from '../../saved_objects/synthetics_monitor/get_all_monitors'; +import { processMonitors } from '../../saved_objects/synthetics_monitor/get_all_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'; @@ -65,6 +63,7 @@ export class StatusRuleExecutor { options: StatusRuleExecutorOptions; logger: Logger; ruleName: string; + monitorConfigRepository: MonitorConfigRepository; constructor( esClient: SyntheticsEsClient, @@ -80,6 +79,10 @@ export class StatusRuleExecutor { this.params = params; this.soClient = savedObjectsClient; this.esClient = esClient; + this.monitorConfigRepository = new MonitorConfigRepository( + savedObjectsClient, + server.encryptedSavedObjects.getClient() + ); this.server = server; this.syntheticsMonitorClient = syntheticsMonitorClient; this.hasCustomCondition = !isEmpty(this.params); @@ -134,8 +137,7 @@ export class StatusRuleExecutor { projects: this.params.projects, }); - this.monitors = await getAllMonitors({ - soClient: this.soClient, + this.monitors = await this.monitorConfigRepository.getAll({ filter: filtersStr, }); 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 024ca7398282..f1487c66076a 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 @@ -12,10 +12,11 @@ import { mockEncryptedSO } from '../../synthetics_service/utils/mocks'; import { elasticsearchClientMock } from '@kbn/core-elasticsearch-client-server-mocks'; import { SyntheticsMonitorClient } from '../../synthetics_service/synthetics_monitor/synthetics_monitor_client'; import { SyntheticsService } from '../../synthetics_service/synthetics_service'; -import * as monitorUtils from '../../saved_objects/synthetics_monitor/get_all_monitors'; import * as locationsUtils from '../../synthetics_service/get_all_locations'; import type { PublicLocation } from '../../../common/runtime_types'; import { SyntheticsServerSetup } from '../../types'; +import { TLSRuleParams } from '@kbn/response-ops-rule-params/synthetics_tls'; +import { ElasticsearchClient, SavedObjectsClientContract } from '@kbn/core/server'; describe('tlsRuleExecutor', () => { const mockEsClient = elasticsearchClientMock.createElasticsearchClient(); @@ -57,27 +58,33 @@ describe('tlsRuleExecutor', () => { const syntheticsService = new SyntheticsService(serverMock); + const commonFilter = + 'synthetics-monitor.attributes.alert.tls.enabled: true and (synthetics-monitor.attributes.type: http or synthetics-monitor.attributes.type: tcp)'; + const monitorClient = new SyntheticsMonitorClient(syntheticsService, serverMock); + const getTLSRuleExecutorParams = ( + ruleParams: TLSRuleParams = {} + ): [ + Date, + TLSRuleParams, + SavedObjectsClientContract, + ElasticsearchClient, + SyntheticsServerSetup, + SyntheticsMonitorClient + ] => [moment().toDate(), ruleParams, soClient, mockEsClient, serverMock, monitorClient]; + it('should only query enabled monitors', async () => { - const spy = jest.spyOn(monitorUtils, 'getAllMonitors').mockResolvedValue([]); - const tlsRule = new TLSRuleExecutor( - moment().toDate(), - {}, - soClient, - mockEsClient, - serverMock, - monitorClient - ); + const tlsRule = new TLSRuleExecutor(...getTLSRuleExecutorParams()); + const configRepo = tlsRule.monitorConfigRepository; + const spy = jest.spyOn(configRepo, 'getAll').mockResolvedValue([]); const { certs } = await tlsRule.getExpiredCertificates(); expect(certs).toEqual([]); expect(spy).toHaveBeenCalledWith({ - filter: - 'synthetics-monitor.attributes.alert.tls.enabled: true and (synthetics-monitor.attributes.type: http or synthetics-monitor.attributes.type: tcp)', - soClient, + filter: commonFilter, }); }); }); 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 b04729492483..598c04fc2269 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 @@ -12,16 +12,14 @@ import { ElasticsearchClient } from '@kbn/core-elasticsearch-server'; import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types'; import type { TLSRuleParams } from '@kbn/response-ops-rule-params/synthetics_tls'; import moment from 'moment'; +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 { - getAllMonitors, - processMonitors, -} from '../../saved_objects/synthetics_monitor/get_all_monitors'; +import { processMonitors } from '../../saved_objects/synthetics_monitor/get_all_monitors'; import { CertResult, ConfigKey, @@ -41,6 +39,7 @@ export class TLSRuleExecutor { server: SyntheticsServerSetup; syntheticsMonitorClient: SyntheticsMonitorClient; monitors: Array> = []; + monitorConfigRepository: MonitorConfigRepository; constructor( previousStartedAt: Date | null, @@ -58,12 +57,15 @@ export class TLSRuleExecutor { }); this.server = server; this.syntheticsMonitorClient = syntheticsMonitorClient; + this.monitorConfigRepository = new MonitorConfigRepository( + soClient, + server.encryptedSavedObjects.getClient() + ); } async getMonitors() { const HTTP_OR_TCP = `${monitorAttributes}.${ConfigKey.MONITOR_TYPE}: http or ${monitorAttributes}.${ConfigKey.MONITOR_TYPE}: tcp`; - this.monitors = await getAllMonitors({ - soClient: this.soClient, + this.monitors = await this.monitorConfigRepository.getAll({ filter: `${monitorAttributes}.${AlertConfigKey.TLS_ENABLED}: true and (${HTTP_OR_TCP})`, }); 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 5fbede652345..a477dba80d63 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 @@ -8,25 +8,31 @@ import * as getAllMonitors from '../../saved_objects/synthetics_monitor/get_all_monitors'; import * as getCerts from '../../queries/get_certs'; import { getSyntheticsCertsRoute } from './get_certificates'; +import { MonitorConfigRepository } from '../../services/monitor_config_repository'; +import { savedObjectsClientMock } from '@kbn/core-saved-objects-api-server-mocks'; +import { encryptedSavedObjectsMock } from '@kbn/encrypted-saved-objects-plugin/server/mocks'; describe('getSyntheticsCertsRoute', () => { - let getMonitorsSpy: jest.SpyInstance; - - beforeEach(() => { - getMonitorsSpy = jest.spyOn(getAllMonitors, 'getAllMonitors').mockReturnValue([] as any); - }); - afterEach(() => jest.clearAllMocks()); + const soClient = savedObjectsClientMock.create(); + const encryptedSavedObjectsClient = encryptedSavedObjectsMock.createStart().getClient(); + + const mockMonitorConfigRepository = new MonitorConfigRepository( + soClient, + encryptedSavedObjectsClient + ); it('returns empty set when no monitors are found', async () => { const route = getSyntheticsCertsRoute(); + mockMonitorConfigRepository.getAll = jest.fn().mockReturnValue([]); expect( await route.handler({ // @ts-expect-error partial implementation for testing request: { query: {} }, // @ts-expect-error partial implementation for testing syntheticsEsClient: jest.fn(), - savedObjectClient: jest.fn(), + savedObjectClient: soClient, + monitorConfigRepository: mockMonitorConfigRepository, }) ).toEqual({ data: { @@ -34,7 +40,7 @@ describe('getSyntheticsCertsRoute', () => { total: 0, }, }); - expect(getMonitorsSpy).toHaveBeenCalledTimes(1); + expect(mockMonitorConfigRepository.getAll).toHaveBeenCalledTimes(1); }); it('returns cert data when monitors are found', async () => { @@ -78,15 +84,17 @@ describe('getSyntheticsCertsRoute', () => { // @ts-expect-error partial implementation for testing .mockReturnValue(getCertsResult); const route = getSyntheticsCertsRoute(); - getMonitorsSpy.mockReturnValue(getMonitorsResult); + const getAll = jest.fn().mockReturnValue(getMonitorsResult); const result = await route.handler({ // @ts-expect-error partial implementation for testing request: { query: {} }, // @ts-expect-error partial implementation for testing syntheticsEsClient: jest.fn(), savedObjectClient: jest.fn(), + // @ts-expect-error partial implementation for testing + monitorConfigRepository: { getAll }, }); - expect(getMonitorsSpy).toHaveBeenCalledTimes(1); + expect(getAll).toHaveBeenCalledTimes(1); expect(processMonitorsSpy).toHaveBeenCalledTimes(1); expect(processMonitorsSpy).toHaveBeenCalledWith(getMonitorsResult); expect(getSyntheticsCertsSpy).toHaveBeenCalledTimes(1); 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 5d6fc1ab61ff..23f44e32bb15 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 @@ -7,10 +7,7 @@ import { schema } from '@kbn/config-schema'; import { SyntheticsRestApiRouteFactory } from '../types'; -import { - getAllMonitors, - processMonitors, -} from '../../saved_objects/synthetics_monitor/get_all_monitors'; +import { processMonitors } from '../../saved_objects/synthetics_monitor/get_all_monitors'; import { monitorAttributes } from '../../../common/types/saved_objects'; import { SYNTHETICS_API_URLS } from '../../../common/constants'; import { CertResult, GetCertsParams } from '../../../common/runtime_types'; @@ -34,11 +31,10 @@ export const getSyntheticsCertsRoute: SyntheticsRestApiRouteFactory< to: schema.maybe(schema.string()), }), }, - handler: async ({ request, syntheticsEsClient, savedObjectsClient }) => { + handler: async ({ request, syntheticsEsClient, monitorConfigRepository }) => { const queryParams = request.query; - const monitors = await getAllMonitors({ - soClient: savedObjectsClient, + const monitors = await monitorConfigRepository.getAll({ filter: `${monitorAttributes}.${ConfigKey.ENABLED}: true`, }); 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 38c196488a15..1e32a4c3cdc1 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 @@ -5,7 +5,6 @@ * 2.0. */ import { SavedObjectsFindResult } from '@kbn/core-saved-objects-api-server'; -import * as monitorsFns from '../../saved_objects/synthetics_monitor/get_all_monitors'; import { EncryptedSyntheticsMonitorAttributes } from '../../../common/runtime_types'; import { getUptimeESMockClient } from '../../queries/test_helpers'; @@ -536,7 +535,7 @@ describe('current status route', () => { [['North America - US Central', 'US Central QA'], 2], [undefined, 2], ])('handles disabled count when using location filters', async (locations, disabledCount) => { - jest.spyOn(monitorsFns, 'getAllMonitors').mockResolvedValue([ + const getAll = jest.fn().mockResolvedValue([ { type: 'synthetics-monitor', id: 'a9a94f2f-47ba-4fe2-afaa-e5cd29b281f1', @@ -691,6 +690,9 @@ describe('current status route', () => { }, }, syntheticsEsClient, + monitorConfigRepository: { + getAll, + }, } as any); const result = await overviewStatusService.getOverviewStatus(); @@ -708,7 +710,7 @@ describe('current status route', () => { [['North America - US Central', 'US Central QA'], 2], [undefined, 2], ])('handles pending count when using location filters', async (locations, pending) => { - jest.spyOn(monitorsFns, 'getAllMonitors').mockResolvedValue([ + const getAll = jest.fn().mockResolvedValue([ { type: 'synthetics-monitor', id: 'a9a94f2f-47ba-4fe2-afaa-e5cd29b281f1', @@ -761,6 +763,9 @@ describe('current status route', () => { }, }, syntheticsEsClient, + monitorConfigRepository: { + getAll, + }, } as any); const result = await overviewStatusService.getOverviewStatus(); 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 e7dd4bdc72be..de6db68fec17 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 @@ -12,10 +12,7 @@ import { isEmpty } from 'lodash'; import { withApmSpan } from '@kbn/apm-data-access-plugin/server/utils/with_apm_span'; import { asMutableArray } from '../../../common/utils/as_mutable_array'; import { getMonitorFilters, OverviewStatusQuery } from '../common'; -import { - getAllMonitors, - processMonitors, -} from '../../saved_objects/synthetics_monitor/get_all_monitors'; +import { processMonitors } from '../../saved_objects/synthetics_monitor/get_all_monitors'; import { ConfigKey } from '../../../common/constants/monitor_management'; import { RouteContext } from '../types'; import { @@ -308,7 +305,7 @@ export class OverviewStatusService { } async getMonitorConfigs() { - const { savedObjectsClient, request } = this.routeContext; + const { request } = this.routeContext; const { query, showFromAllSpaces } = request.query || {}; /** * Walk through all monitor saved objects, bucket IDs by disabled/enabled status. @@ -319,8 +316,7 @@ export class OverviewStatusService { const { filtersStr } = this.filterData; - return await getAllMonitors({ - soClient: savedObjectsClient, + return this.routeContext.monitorConfigRepository.getAll({ showFromAllSpaces, search: query ? `${query}*` : '', filter: filtersStr, diff --git a/x-pack/solutions/observability/plugins/synthetics/server/routes/settings/sync_global_params.ts b/x-pack/solutions/observability/plugins/synthetics/server/routes/settings/sync_global_params.ts index de9111ea00b1..05e6a2860b2c 100644 --- a/x-pack/solutions/observability/plugins/synthetics/server/routes/settings/sync_global_params.ts +++ b/x-pack/solutions/observability/plugins/synthetics/server/routes/settings/sync_global_params.ts @@ -27,6 +27,7 @@ export const syncParamsSyntheticsParamsRoute: SyntheticsRestApiRouteFactory = () await syntheticsMonitorClient.syncGlobalParams({ spaceId, allPrivateLocations, + soClient: savedObjectsClient, encryptedSavedObjects: server.encryptedSavedObjects, }); diff --git a/x-pack/solutions/observability/plugins/synthetics/server/routes/types.ts b/x-pack/solutions/observability/plugins/synthetics/server/routes/types.ts index 837197d72d23..0ad30ca3fdcf 100644 --- a/x-pack/solutions/observability/plugins/synthetics/server/routes/types.ts +++ b/x-pack/solutions/observability/plugins/synthetics/server/routes/types.ts @@ -21,6 +21,7 @@ import { HttpResponsePayload, ResponseError, } from '@kbn/core-http-server'; +import { MonitorConfigRepository } from '../services/monitor_config_repository'; import { SyntheticsEsClient } from '../lib'; import { SyntheticsServerSetup, UptimeRequestHandlerContext } from '../types'; import { SyntheticsMonitorClient } from '../synthetics_service/synthetics_monitor/synthetics_monitor_client'; @@ -108,6 +109,7 @@ export interface RouteContext< syntheticsMonitorClient: SyntheticsMonitorClient; subject?: Subject; spaceId: string; + monitorConfigRepository: MonitorConfigRepository; } export type SyntheticsRouteHandler< 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/get_all_monitors.ts index ebf034d9eb7f..673b0e6e9586 100644 --- 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/get_all_monitors.ts @@ -5,60 +5,15 @@ * 2.0. */ -import { - SavedObjectsClientContract, - SavedObjectsFindOptions, - SavedObjectsFindResult, -} from '@kbn/core-saved-objects-api-server'; +import { SavedObjectsFindResult } from '@kbn/core-saved-objects-api-server'; import { intersection } from 'lodash'; -import { withApmSpan } from '@kbn/apm-data-access-plugin/server/utils'; import { periodToMs } from '../../routes/overview_status/utils'; -import { syntheticsMonitorType } from '../../../common/types/saved_objects'; import { ConfigKey, EncryptedSyntheticsMonitorAttributes, SourceType, } from '../../../common/runtime_types'; -export const getAllMonitors = async ({ - soClient, - search, - fields, - filter, - sortField = 'name.keyword', - sortOrder = 'asc', - searchFields, - showFromAllSpaces, -}: { - soClient: SavedObjectsClientContract; - search?: string; - filter?: string; - showFromAllSpaces?: boolean; -} & Pick) => { - return withApmSpan('get_all_monitors', async () => { - const finder = soClient.createPointInTimeFinder({ - type: syntheticsMonitorType, - perPage: 5000, - search, - sortField, - sortOrder, - fields, - filter, - searchFields, - ...(showFromAllSpaces && { namespaces: ['*'] }), - }); - - const hits: Array> = []; - for await (const result of finder.find()) { - hits.push(...result.saved_objects); - } - - finder.close().catch(() => {}); - - return hits; - }); -}; - export const processMonitors = ( allMonitors: Array>, queryLocations?: string[] | string 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 new file mode 100644 index 000000000000..a612f4b0cb37 --- /dev/null +++ b/x-pack/solutions/observability/plugins/synthetics/server/services/monitor_config_repository.test.ts @@ -0,0 +1,606 @@ +/* + * 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 { 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'; + +// Mock the utils functions +jest.mock('../synthetics_service/utils', () => ({ + formatSecrets: jest.fn((data) => ({ ...data, formattedSecrets: true })), + normalizeSecrets: jest.fn((data) => ({ ...data, normalizedSecrets: true })), +})); + +// Mock the AMP span +jest.mock('@kbn/apm-data-access-plugin/server/utils/with_apm_span', () => ({ + withApmSpan: jest.fn((spanName, fn) => fn()), +})); + +describe('MonitorConfigRepository', () => { + let soClient: jest.Mocked; + let encryptedSavedObjectsClient: jest.Mocked; + let repository: MonitorConfigRepository; + + beforeEach(() => { + soClient = savedObjectsClientMock.create(); + encryptedSavedObjectsClient = encryptedSavedObjectsMock + .createStart() + .getClient() as jest.Mocked; + repository = new MonitorConfigRepository(soClient, encryptedSavedObjectsClient); + + // Clear all mocks before each test + jest.clearAllMocks(); + }); + + describe('get', () => { + it('should get a monitor by id', async () => { + const id = 'test-id'; + const mockMonitor = { + id, + attributes: { name: 'Test Monitor' }, + type: syntheticsMonitorType, + references: [], + }; + + soClient.get.mockResolvedValue(mockMonitor); + + const result = await repository.get(id); + + expect(soClient.get).toHaveBeenCalledWith(syntheticsMonitorType, id); + expect(result).toBe(mockMonitor); + }); + + it('should propagate errors', async () => { + const id = 'test-id'; + const error = new Error('Not found'); + + soClient.get.mockRejectedValue(error); + + await expect(repository.get(id)).rejects.toThrow(error); + }); + }); + + describe('getDecrypted', () => { + it('should get and decrypt a monitor by id and space', async () => { + const id = 'test-id'; + const spaceId = 'test-space'; + const mockDecryptedMonitor = { + id, + attributes: { name: 'Test Monitor', secrets: 'decrypted' }, + type: syntheticsMonitorType, + references: [], + }; + + encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue( + mockDecryptedMonitor + ); + (utils.normalizeSecrets as jest.Mock).mockReturnValue({ + ...mockDecryptedMonitor, + normalizedSecrets: true, + }); + + const result = await repository.getDecrypted(id, spaceId); + + expect(encryptedSavedObjectsClient.getDecryptedAsInternalUser).toHaveBeenCalledWith( + syntheticsMonitorType, + id, + { namespace: spaceId } + ); + expect(utils.normalizeSecrets).toHaveBeenCalledWith(mockDecryptedMonitor); + expect(result).toEqual({ ...mockDecryptedMonitor, normalizedSecrets: true }); + }); + }); + + describe('create', () => { + it('should create a monitor with an id', async () => { + const id = 'test-id'; + const normalizedMonitor = { + name: 'Test Monitor', + [ConfigKey.CUSTOM_HEARTBEAT_ID]: 'custom-id', + } as unknown as SyntheticsMonitor; + + const mockCreatedMonitor = { + id, + attributes: { name: 'Test Monitor' }, + type: syntheticsMonitorType, + references: [], + }; + soClient.create.mockResolvedValue(mockCreatedMonitor); + + const result = await repository.create({ + id, + normalizedMonitor, + }); + + expect(utils.formatSecrets).toHaveBeenCalledWith({ + ...normalizedMonitor, + [ConfigKey.MONITOR_QUERY_ID]: 'custom-id', + [ConfigKey.CONFIG_ID]: id, + revision: 1, + }); + + expect(soClient.create).toHaveBeenCalledWith( + syntheticsMonitorType, + { + ...normalizedMonitor, + [ConfigKey.MONITOR_QUERY_ID]: 'custom-id', + [ConfigKey.CONFIG_ID]: id, + revision: 1, + formattedSecrets: true, + }, + { id, overwrite: true } + ); + + expect(result).toBe(mockCreatedMonitor); + }); + + it('should create a monitor without an id', async () => { + const normalizedMonitor = { + name: 'Test Monitor', + } as unknown as SyntheticsMonitor; + + const mockCreatedMonitor = { + id: 'generated-id', + attributes: { name: 'Test Monitor' }, + type: syntheticsMonitorType, + references: [], + }; + soClient.create.mockResolvedValue(mockCreatedMonitor); + + const result = await repository.create({ + id: '', + normalizedMonitor, + }); + + expect(utils.formatSecrets).toHaveBeenCalledWith({ + ...normalizedMonitor, + [ConfigKey.MONITOR_QUERY_ID]: '', + [ConfigKey.CONFIG_ID]: '', + revision: 1, + }); + + expect(soClient.create).toHaveBeenCalledWith( + syntheticsMonitorType, + { + ...normalizedMonitor, + [ConfigKey.MONITOR_QUERY_ID]: '', + [ConfigKey.CONFIG_ID]: '', + revision: 1, + formattedSecrets: true, + }, + undefined + ); + + expect(result).toBe(mockCreatedMonitor); + }); + }); + + describe('createBulk', () => { + it('should create multiple monitors in bulk', async () => { + const monitors = [ + { + id: 'test-id-1', + monitor: { + name: 'Test Monitor 1', + [ConfigKey.CUSTOM_HEARTBEAT_ID]: 'custom-id-1', + }, + }, + { + id: 'test-id-2', + monitor: { + name: 'Test Monitor 2', + }, + }, + ] as any; + + const mockBulkCreateResult = { + saved_objects: [ + { + id: 'test-id-1', + attributes: { name: 'Test Monitor 1' }, + type: syntheticsMonitorType, + references: [], + }, + { + id: 'test-id-2', + attributes: { name: 'Test Monitor 2' }, + type: syntheticsMonitorType, + references: [], + }, + ], + }; + + soClient.bulkCreate.mockResolvedValue(mockBulkCreateResult); + + const result = await repository.createBulk({ monitors }); + + expect(soClient.bulkCreate).toHaveBeenCalledWith([ + { + id: 'test-id-1', + type: syntheticsMonitorType, + attributes: { + name: 'Test Monitor 1', + [ConfigKey.CUSTOM_HEARTBEAT_ID]: 'custom-id-1', + [ConfigKey.MONITOR_QUERY_ID]: 'custom-id-1', + [ConfigKey.CONFIG_ID]: 'test-id-1', + revision: 1, + formattedSecrets: true, + }, + }, + { + id: 'test-id-2', + type: syntheticsMonitorType, + attributes: { + name: 'Test Monitor 2', + [ConfigKey.MONITOR_QUERY_ID]: 'test-id-2', + [ConfigKey.CONFIG_ID]: 'test-id-2', + revision: 1, + formattedSecrets: true, + }, + }, + ]); + + expect(result).toBe(mockBulkCreateResult.saved_objects); + }); + }); + + describe('bulkUpdate', () => { + it('should update multiple monitors in bulk', async () => { + const monitors = [ + { + id: 'test-id-1', + attributes: { + name: 'Updated Monitor 1', + }, + }, + { + id: 'test-id-2', + attributes: { + name: 'Updated Monitor 2', + }, + }, + ] as any; + + const mockBulkUpdateResult = { + saved_objects: [ + { + id: 'test-id-1', + attributes: { name: 'Updated Monitor 1' }, + type: syntheticsMonitorType, + references: [], + }, + { + id: 'test-id-2', + attributes: { name: 'Updated Monitor 2' }, + type: syntheticsMonitorType, + references: [], + }, + ], + }; + + soClient.bulkUpdate.mockResolvedValue(mockBulkUpdateResult); + + const result = await repository.bulkUpdate({ monitors }); + + expect(soClient.bulkUpdate).toHaveBeenCalledWith([ + { + type: syntheticsMonitorType, + id: 'test-id-1', + attributes: { name: 'Updated Monitor 1' }, + }, + { + type: syntheticsMonitorType, + id: 'test-id-2', + attributes: { name: 'Updated Monitor 2' }, + }, + ]); + + expect(result).toBe(mockBulkUpdateResult); + }); + }); + + describe('find', () => { + it('should find monitors with options', async () => { + const options = { + search: 'test', + page: 1, + perPage: 10, + sortField: 'name', + sortOrder: 'asc' as const, + }; + + const mockFindResult = { + saved_objects: [ + { + id: 'test-id-1', + attributes: { name: 'Test Monitor 1' }, + type: syntheticsMonitorType, + references: [], + }, + { + id: 'test-id-2', + attributes: { name: 'Test Monitor 2' }, + type: syntheticsMonitorType, + references: [], + }, + ], + total: 2, + per_page: 10, + page: 1, + } as any; + + soClient.find.mockResolvedValue(mockFindResult); + + const result = await repository.find(options); + + expect(soClient.find).toHaveBeenCalledWith({ + type: syntheticsMonitorType, + ...options, + }); + + expect(result).toBe(mockFindResult); + }); + + it('should use default perPage if not provided', async () => { + const options = { + search: 'test', + }; + + const mockFindResult = { + saved_objects: [], + total: 0, + per_page: 5000, + page: 1, + }; + + soClient.find.mockResolvedValue(mockFindResult); + + await repository.find(options); + + expect(soClient.find).toHaveBeenCalledWith({ + type: syntheticsMonitorType, + search: 'test', + perPage: 5000, + }); + }); + }); + + describe('findDecryptedMonitors', () => { + it('should find decrypted monitors by space id and filter', async () => { + const spaceId = 'test-space'; + const filter = 'attributes.name:test'; + + const mockDecryptedMonitors = [ + { + id: 'test-id-1', + attributes: { name: 'Test Monitor 1', secrets: 'decrypted' }, + type: syntheticsMonitorType, + references: [], + }, + { + id: 'test-id-2', + attributes: { name: 'Test Monitor 2', secrets: 'decrypted' }, + type: syntheticsMonitorType, + references: [], + }, + ]; + + const pointInTimeFinderMock = { + find: jest.fn().mockImplementation(function* () { + yield { saved_objects: mockDecryptedMonitors }; + }), + close: jest.fn().mockResolvedValue(undefined), + } as any; + + encryptedSavedObjectsClient.createPointInTimeFinderDecryptedAsInternalUser.mockReturnValue( + pointInTimeFinderMock + ); + + const result = await repository.findDecryptedMonitors({ spaceId, filter }); + + expect( + encryptedSavedObjectsClient.createPointInTimeFinderDecryptedAsInternalUser + ).toHaveBeenCalledWith({ + filter, + type: syntheticsMonitorType, + perPage: 500, + namespaces: [spaceId], + }); + + expect(pointInTimeFinderMock.find).toHaveBeenCalled(); + expect(pointInTimeFinderMock.close).toHaveBeenCalled(); + expect(result).toEqual(mockDecryptedMonitors); + }); + + it('should handle finder.close errors', async () => { + const spaceId = 'test-space'; + + const mockDecryptedMonitors = [ + { + id: 'test-id-1', + attributes: { name: 'Test Monitor 1', secrets: 'decrypted' }, + type: syntheticsMonitorType, + references: [], + }, + ]; + + const pointInTimeFinderMock = { + find: jest.fn().mockImplementation(function* () { + yield { saved_objects: mockDecryptedMonitors }; + }), + close: jest.fn().mockRejectedValue(new Error('Close failed')), + } as any; + + encryptedSavedObjectsClient.createPointInTimeFinderDecryptedAsInternalUser.mockReturnValue( + pointInTimeFinderMock + ); + + const result = await repository.findDecryptedMonitors({ spaceId }); + + expect(pointInTimeFinderMock.close).toHaveBeenCalled(); + expect(result).toEqual(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 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(result).toBe(mockBulkDeleteResult); + }); + }); + + describe('getAll', () => { + it('should get all monitors with options', async () => { + const options = { + search: 'test', + fields: ['name'], + filter: 'attributes.enabled:true', + sortField: 'name.keyword', + sortOrder: 'asc' as const, + searchFields: ['name'], + showFromAllSpaces: true, + }; + + const mockMonitors = [ + { id: 'test-id-1', attributes: { name: 'Test Monitor 1' } }, + { id: 'test-id-2', attributes: { name: 'Test Monitor 2' } }, + ]; + + const pointInTimeFinderMock = { + find: jest.fn().mockImplementation(function* () { + yield { saved_objects: mockMonitors }; + }), + close: jest.fn().mockResolvedValue(undefined), + }; + + soClient.createPointInTimeFinder.mockReturnValue(pointInTimeFinderMock); + + const result = await repository.getAll(options); + + expect(soClient.createPointInTimeFinder).toHaveBeenCalledWith({ + type: syntheticsMonitorType, + perPage: 5000, + search: 'test', + fields: ['name'], + filter: 'attributes.enabled:true', + sortField: 'name.keyword', + sortOrder: 'asc', + searchFields: ['name'], + namespaces: ['*'], + }); + + expect(result).toEqual(mockMonitors); + }); + + it('should not include namespaces if showFromAllSpaces is false', async () => { + const options = { + search: 'test', + showFromAllSpaces: false, + }; + + const mockMonitors: any = []; + + const pointInTimeFinderMock = { + find: jest.fn().mockImplementation(function* () { + yield { saved_objects: mockMonitors }; + }), + close: jest.fn().mockResolvedValue(undefined), + }; + + soClient.createPointInTimeFinder.mockReturnValue(pointInTimeFinderMock); + + await repository.getAll(options); + + expect(soClient.createPointInTimeFinder).toHaveBeenCalledWith({ + type: syntheticsMonitorType, + perPage: 5000, + search: 'test', + sortField: 'name.keyword', + sortOrder: 'asc', + }); + }); + + it('should use default sort options if not provided', async () => { + const options = { + search: 'test', + }; + + const mockMonitors: any = []; + + const pointInTimeFinderMock = { + find: jest.fn().mockImplementation(function* () { + yield { saved_objects: mockMonitors }; + }), + close: jest.fn().mockResolvedValue(undefined), + }; + + soClient.createPointInTimeFinder.mockReturnValue(pointInTimeFinderMock); + + await repository.getAll(options); + + expect(soClient.createPointInTimeFinder).toHaveBeenCalledWith({ + type: syntheticsMonitorType, + perPage: 5000, + search: 'test', + sortField: 'name.keyword', + sortOrder: 'asc', + }); + }); + + it('should handle finder.close errors', async () => { + const options = { search: 'test' }; + + const mockMonitors = [{ id: 'test-id-1', attributes: { name: 'Test Monitor 1' } }]; + + const pointInTimeFinderMock = { + find: jest.fn().mockImplementation(function* () { + yield { saved_objects: mockMonitors }; + }), + close: jest.fn().mockRejectedValue(new Error('Close failed')), + }; + + soClient.createPointInTimeFinder.mockReturnValue(pointInTimeFinderMock); + + const result = await repository.getAll(options); + + expect(pointInTimeFinderMock.close).toHaveBeenCalled(); + expect(result).toEqual(mockMonitors); + // Should not throw an error when close fails + }); + }); +}); 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 new file mode 100644 index 000000000000..5c8901d802e9 --- /dev/null +++ b/x-pack/solutions/observability/plugins/synthetics/server/services/monitor_config_repository.ts @@ -0,0 +1,178 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + SavedObject, + SavedObjectsClientContract, + SavedObjectsFindOptions, + 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 { formatSecrets, normalizeSecrets } from '../synthetics_service/utils'; +import { syntheticsMonitorType } from '../../common/types/saved_objects'; +import { + ConfigKey, + EncryptedSyntheticsMonitorAttributes, + MonitorFields, + SyntheticsMonitor, + SyntheticsMonitorWithSecretsAttributes, +} from '../../common/runtime_types'; + +export class MonitorConfigRepository { + constructor( + private soClient: SavedObjectsClientContract, + private encryptedSavedObjectsClient: EncryptedSavedObjectsClient + ) {} + + async get(id: string) { + return await this.soClient.get(syntheticsMonitorType, id); + } + + async getDecrypted(id: string, spaceId: string): Promise> { + const decryptedMonitor = + await this.encryptedSavedObjectsClient.getDecryptedAsInternalUser( + syntheticsMonitorType, + id, + { + namespace: spaceId, + } + ); + return normalizeSecrets(decryptedMonitor); + } + + async create({ id, normalizedMonitor }: { id: string; normalizedMonitor: SyntheticsMonitor }) { + return await this.soClient.create( + syntheticsMonitorType, + formatSecrets({ + ...normalizedMonitor, + [ConfigKey.MONITOR_QUERY_ID]: normalizedMonitor[ConfigKey.CUSTOM_HEARTBEAT_ID] || id, + [ConfigKey.CONFIG_ID]: id, + revision: 1, + }), + id + ? { + id, + overwrite: true, + } + : undefined + ); + } + + async createBulk({ monitors }: { monitors: Array<{ id: string; monitor: MonitorFields }> }) { + const newMonitors = monitors.map(({ id, monitor }) => ({ + id, + type: syntheticsMonitorType, + attributes: formatSecrets({ + ...monitor, + [ConfigKey.MONITOR_QUERY_ID]: monitor[ConfigKey.CUSTOM_HEARTBEAT_ID] || id, + [ConfigKey.CONFIG_ID]: id, + revision: 1, + }), + })); + const result = await this.soClient.bulkCreate( + newMonitors + ); + return result.saved_objects; + } + + async bulkUpdate({ + monitors, + }: { + monitors: Array<{ + attributes: MonitorFields; + id: string; + }>; + }) { + return await this.soClient.bulkUpdate( + monitors.map(({ attributes, id }) => ({ + type: syntheticsMonitorType, + id, + attributes, + })) + ); + } + + find(options: Omit) { + return this.soClient.find({ + type: syntheticsMonitorType, + ...options, + perPage: options.perPage ?? 5000, + }); + } + + async findDecryptedMonitors({ spaceId, filter }: { spaceId: string; filter?: string }) { + const finder = + await this.encryptedSavedObjectsClient.createPointInTimeFinderDecryptedAsInternalUser( + { + filter, + type: syntheticsMonitorType, + perPage: 500, + namespaces: [spaceId], + } + ); + + const decryptedMonitors: Array> = + []; + for await (const result of finder.find()) { + decryptedMonitors.push(...result.saved_objects); + } + + finder.close().catch(() => {}); + + return decryptedMonitors; + } + + 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 getAll< + T extends EncryptedSyntheticsMonitorAttributes = EncryptedSyntheticsMonitorAttributes + >({ + search, + fields, + filter, + sortField = 'name.keyword', + sortOrder = 'asc', + searchFields, + showFromAllSpaces, + }: { + search?: string; + filter?: string; + showFromAllSpaces?: boolean; + } & Pick) { + return withApmSpan('get_all_monitors', async () => { + const finder = this.soClient.createPointInTimeFinder({ + type: syntheticsMonitorType, + perPage: 5000, + search, + sortField, + sortOrder, + fields, + filter, + searchFields, + ...(showFromAllSpaces && { namespaces: ['*'] }), + }); + + const hits: Array> = []; + for await (const result of finder.find()) { + hits.push(...result.saved_objects); + } + + finder.close().catch(() => {}); + + return hits; + }); + } +} diff --git a/x-pack/solutions/observability/plugins/synthetics/server/synthetics_route_wrapper.ts b/x-pack/solutions/observability/plugins/synthetics/server/synthetics_route_wrapper.ts index 24abf55d53c4..5cfcf2aada8d 100644 --- a/x-pack/solutions/observability/plugins/synthetics/server/synthetics_route_wrapper.ts +++ b/x-pack/solutions/observability/plugins/synthetics/server/synthetics_route_wrapper.ts @@ -8,6 +8,7 @@ import { withApmSpan } from '@kbn/apm-data-access-plugin/server/utils/with_apm_s import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common'; import { isEmpty } from 'lodash'; import { isKibanaResponse } from '@kbn/core-http-server'; +import { MonitorConfigRepository } from './services/monitor_config_repository'; import { syntheticsServiceApiKey } from './saved_objects/service_api_key'; import { isTestUser, SyntheticsEsClient } from './lib'; import { SYNTHETICS_INDEX_PATTERN } from '../common/constants'; @@ -56,6 +57,11 @@ export const syntheticsRouteWrapper: SyntheticsRouteWrapper = ( ); server.syntheticsEsClient = syntheticsEsClient; + const encryptedSavedObjectsClient = server.encryptedSavedObjects.getClient(); + const monitorConfigRepository = new MonitorConfigRepository( + savedObjectsClient, + encryptedSavedObjectsClient + ); const spaceId = server.spaces?.spacesService.getSpaceId(request) ?? DEFAULT_SPACE_ID; @@ -69,6 +75,7 @@ export const syntheticsRouteWrapper: SyntheticsRouteWrapper = ( server, spaceId, syntheticsMonitorClient, + monitorConfigRepository, }); if (isKibanaResponse(res)) { return res; diff --git a/x-pack/solutions/observability/plugins/synthetics/server/synthetics_service/sync_global_params.ts b/x-pack/solutions/observability/plugins/synthetics/server/synthetics_service/sync_global_params.ts index 7bd22fb8a506..d90ca0eec524 100644 --- a/x-pack/solutions/observability/plugins/synthetics/server/synthetics_service/sync_global_params.ts +++ b/x-pack/solutions/observability/plugins/synthetics/server/synthetics_service/sync_global_params.ts @@ -33,6 +33,7 @@ export const syncSpaceGlobalParams = async ({ await syntheticsMonitorClient.syncGlobalParams({ spaceId, allPrivateLocations, + soClient: savedObjectsClient, encryptedSavedObjects, }); logger.debug(`Sync of global parameters for space with id ${spaceId} succeeded`); 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 b0f9cd20211b..c7f2ffc73daf 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 @@ -6,6 +6,7 @@ */ import { SavedObject, SavedObjectsClientContract, SavedObjectsFindResult } from '@kbn/core/server'; import { EncryptedSavedObjectsPluginStart } from '@kbn/encrypted-saved-objects-plugin/server'; +import { MonitorConfigRepository } from '../../services/monitor_config_repository'; import { SyntheticsServerSetup } from '../../types'; import { syntheticsMonitorType } from '../../../common/types/saved_objects'; import { normalizeSecrets } from '../utils'; @@ -274,8 +275,10 @@ export class SyntheticsMonitorClient { spaceId, allPrivateLocations, encryptedSavedObjects, + soClient, }: { spaceId: string; + soClient: SavedObjectsClientContract; allPrivateLocations: PrivateLocationAttributes[]; encryptedSavedObjects: EncryptedSavedObjectsPluginStart; }) { @@ -285,6 +288,7 @@ export class SyntheticsMonitorClient { const { allConfigs: monitors, paramsBySpace } = await this.getAllMonitorConfigs({ encryptedSavedObjects, + soClient, spaceId, }); @@ -309,14 +313,20 @@ export class SyntheticsMonitorClient { async getAllMonitorConfigs({ spaceId, + soClient, encryptedSavedObjects, }: { spaceId: string; + soClient: SavedObjectsClientContract; encryptedSavedObjects: EncryptedSavedObjectsPluginStart; }) { const paramsBySpacePromise = this.syntheticsService.getSyntheticsParams({ spaceId }); + const monitorConfigRepository = new MonitorConfigRepository( + soClient, + encryptedSavedObjects.getClient() + ); - const monitorsPromise = this.getAllMonitors({ encryptedSavedObjects, spaceId }); + const monitorsPromise = monitorConfigRepository.findDecryptedMonitors({ spaceId }); const [paramsBySpace, monitors] = await Promise.all([paramsBySpacePromise, monitorsPromise]);