[Synthetics] Added monitor config repository (#221089)

The monitor config repository was created in [this
PR](https://github.com/elastic/kibana/pull/202325) but not backported to
any other kibana version.

This is causing many failures when backporting PRs involving Synthetics
code.

The goal of this PR is to align older versions with main and avoid
future conflicts with backports.
This commit is contained in:
Francesco Fagnani 2025-05-22 08:45:31 +02:00 committed by GitHub
parent cb5a994b33
commit 23f98ea25b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 900 additions and 123 deletions

View file

@ -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 () => {

View file

@ -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 () => {

View file

@ -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 () => {

View file

@ -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 () => {

View file

@ -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');

View file

@ -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 () => {

View file

@ -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 () => {

View file

@ -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 () => {

View file

@ -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: {

View file

@ -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,
});

View file

@ -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,
});
});
});

View file

@ -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<SavedObjectsFindResult<EncryptedSyntheticsMonitorAttributes>> = [];
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})`,
});

View file

@ -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);

View file

@ -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`,
});

View file

@ -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();

View file

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

View file

@ -27,6 +27,7 @@ export const syncParamsSyntheticsParamsRoute: SyntheticsRestApiRouteFactory = ()
await syntheticsMonitorClient.syncGlobalParams({
spaceId,
allPrivateLocations,
soClient: savedObjectsClient,
encryptedSavedObjects: server.encryptedSavedObjects,
});

View file

@ -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<unknown>;
spaceId: string;
monitorConfigRepository: MonitorConfigRepository;
}
export type SyntheticsRouteHandler<

View file

@ -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<SavedObjectsFindOptions, 'sortField' | 'sortOrder' | 'fields' | 'searchFields'>) => {
return withApmSpan('get_all_monitors', async () => {
const finder = soClient.createPointInTimeFinder<EncryptedSyntheticsMonitorAttributes>({
type: syntheticsMonitorType,
perPage: 5000,
search,
sortField,
sortOrder,
fields,
filter,
searchFields,
...(showFromAllSpaces && { namespaces: ['*'] }),
});
const hits: Array<SavedObjectsFindResult<EncryptedSyntheticsMonitorAttributes>> = [];
for await (const result of finder.find()) {
hits.push(...result.saved_objects);
}
finder.close().catch(() => {});
return hits;
});
};
export const processMonitors = (
allMonitors: Array<SavedObjectsFindResult<EncryptedSyntheticsMonitorAttributes>>,
queryLocations?: string[] | string

View file

@ -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<SavedObjectsClientContract>;
let encryptedSavedObjectsClient: jest.Mocked<EncryptedSavedObjectsClient>;
let repository: MonitorConfigRepository;
beforeEach(() => {
soClient = savedObjectsClientMock.create();
encryptedSavedObjectsClient = encryptedSavedObjectsMock
.createStart()
.getClient() as jest.Mocked<EncryptedSavedObjectsClient>;
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
});
});
});

View file

@ -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<EncryptedSyntheticsMonitorAttributes>(syntheticsMonitorType, id);
}
async getDecrypted(id: string, spaceId: string): Promise<SavedObject<SyntheticsMonitor>> {
const decryptedMonitor =
await this.encryptedSavedObjectsClient.getDecryptedAsInternalUser<SyntheticsMonitorWithSecretsAttributes>(
syntheticsMonitorType,
id,
{
namespace: spaceId,
}
);
return normalizeSecrets(decryptedMonitor);
}
async create({ id, normalizedMonitor }: { id: string; normalizedMonitor: SyntheticsMonitor }) {
return await this.soClient.create<EncryptedSyntheticsMonitorAttributes>(
syntheticsMonitorType,
formatSecrets({
...normalizedMonitor,
[ConfigKey.MONITOR_QUERY_ID]: normalizedMonitor[ConfigKey.CUSTOM_HEARTBEAT_ID] || id,
[ConfigKey.CONFIG_ID]: id,
revision: 1,
}),
id
? {
id,
overwrite: true,
}
: undefined
);
}
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<EncryptedSyntheticsMonitorAttributes>(
newMonitors
);
return result.saved_objects;
}
async bulkUpdate({
monitors,
}: {
monitors: Array<{
attributes: MonitorFields;
id: string;
}>;
}) {
return await this.soClient.bulkUpdate<MonitorFields>(
monitors.map(({ attributes, id }) => ({
type: syntheticsMonitorType,
id,
attributes,
}))
);
}
find<T>(options: Omit<SavedObjectsFindOptions, 'type'>) {
return this.soClient.find<T>({
type: syntheticsMonitorType,
...options,
perPage: options.perPage ?? 5000,
});
}
async findDecryptedMonitors({ spaceId, filter }: { spaceId: string; filter?: string }) {
const finder =
await this.encryptedSavedObjectsClient.createPointInTimeFinderDecryptedAsInternalUser<SyntheticsMonitorWithSecretsAttributes>(
{
filter,
type: syntheticsMonitorType,
perPage: 500,
namespaces: [spaceId],
}
);
const decryptedMonitors: Array<SavedObjectsFindResult<SyntheticsMonitorWithSecretsAttributes>> =
[];
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<SavedObjectsFindOptions, 'sortField' | 'sortOrder' | 'fields' | 'searchFields'>) {
return withApmSpan('get_all_monitors', async () => {
const finder = this.soClient.createPointInTimeFinder<T>({
type: syntheticsMonitorType,
perPage: 5000,
search,
sortField,
sortOrder,
fields,
filter,
searchFields,
...(showFromAllSpaces && { namespaces: ['*'] }),
});
const hits: Array<SavedObjectsFindResult<T>> = [];
for await (const result of finder.find()) {
hits.push(...result.saved_objects);
}
finder.close().catch(() => {});
return hits;
});
}
}

View file

@ -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;

View file

@ -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`);

View file

@ -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]);