[Synthetics] Refactor: Create monitor configs repository !! (#202325)

## Summary

Create monitor configs repository around monitor saved object to make
sure all operations are performed from same class.

This will be helpful when we create a new saved object to support
multiple-spaces !!

### Testing
All unit tests, api tests passing should be more than enough !!

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Shahzad 2025-03-12 10:43:25 +01:00 committed by GitHub
parent b0ef1e6365
commit 9100170e29
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
34 changed files with 959 additions and 434 deletions

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,12 +94,11 @@ describe('StatusRuleExecutor', () => {
expect(spy).toHaveBeenCalledWith({
filter: 'synthetics-monitor.attributes.alert.status.enabled: true',
soClient,
});
});
it('marks deleted configs as expected', async () => {
jest.spyOn(monitorUtils, 'getAllMonitors').mockResolvedValue(testMonitors);
jest.spyOn(configRepo, 'getAll').mockResolvedValue(testMonitors);
const { downConfigs } = await statusRule.getDownChecks({});
@ -175,7 +174,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 } 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);
@ -125,8 +128,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,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';
@ -60,7 +59,6 @@ describe('tlsRuleExecutor', () => {
const monitorClient = new SyntheticsMonitorClient(syntheticsService, serverMock);
it('should only query enabled monitors', async () => {
const spy = jest.spyOn(monitorUtils, 'getAllMonitors').mockResolvedValue([]);
const tlsRule = new TLSRuleExecutor(
moment().toDate(),
{},
@ -69,6 +67,8 @@ describe('tlsRuleExecutor', () => {
serverMock,
monitorClient
);
const configRepo = tlsRule.monitorConfigRepository;
const spy = jest.spyOn(configRepo, 'getAll').mockResolvedValue([]);
const { certs } = await tlsRule.getExpiredCertificates();
@ -77,7 +77,6 @@ describe('tlsRuleExecutor', () => {
expect(spy).toHaveBeenCalledWith({
filter:
'synthetics-monitor.attributes.alert.tls.enabled: true and (synthetics-monitor.attributes.type: http or synthetics-monitor.attributes.type: tcp)',
soClient,
});
});
});

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

@ -4,36 +4,3 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { SavedObject } from '@kbn/core/server';
import { EncryptedSavedObjectsClient } from '@kbn/encrypted-saved-objects-plugin/server';
import { syntheticsMonitorType } from '../../common/types/saved_objects';
import {
SyntheticsMonitorWithSecretsAttributes,
SyntheticsMonitor,
} from '../../common/runtime_types';
import { normalizeSecrets } from '../synthetics_service/utils/secrets';
export const getSyntheticsMonitor = async ({
monitorId,
encryptedSavedObjectsClient,
spaceId,
}: {
monitorId: string;
spaceId: string;
encryptedSavedObjectsClient: EncryptedSavedObjectsClient;
}): Promise<SavedObject<SyntheticsMonitor>> => {
try {
const decryptedMonitor =
await encryptedSavedObjectsClient.getDecryptedAsInternalUser<SyntheticsMonitorWithSecretsAttributes>(
syntheticsMonitorType,
monitorId,
{
namespace: spaceId,
}
);
return normalizeSecrets(decryptedMonitor);
} catch (e) {
throw e;
}
};

View file

@ -7,7 +7,7 @@
import { IKibanaResponse } from '@kbn/core-http-server';
import { getJourneyScreenshot, ScreenshotReturnTypesUnion } from './get_journey_screenshot';
import { isRefResult, RefResult } from '../../common/runtime_types';
import { RouteContext, UptimeRouteContext } from '../routes/types';
import { RouteContext } from '../routes/types';
export interface ClientContract {
screenshotRef: RefResult;
@ -25,7 +25,7 @@ export const journeyScreenshotHandler = async ({
response,
request,
syntheticsEsClient,
}: RouteContext | UptimeRouteContext): Promise<IKibanaResponse<ClientContract>> => {
}: RouteContext): Promise<IKibanaResponse<ClientContract>> => {
const { checkGroup, stepIndex } = request.params;
const result: ScreenshotReturnTypesUnion | null = await getJourneyScreenshot({

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

@ -15,7 +15,7 @@ import { MonitorSortFieldSchema } from '../../common/runtime_types/monitor_manag
import { getAllLocations } from '../synthetics_service/get_all_locations';
import { EncryptedSyntheticsMonitorAttributes } from '../../common/runtime_types';
import { PrivateLocation, ServiceLocation } from '../../common/runtime_types';
import { monitorAttributes, syntheticsMonitorType } from '../../common/types/saved_objects';
import { monitorAttributes } from '../../common/types/saved_objects';
const StringOrArraySchema = schema.maybe(
schema.oneOf([schema.string(), schema.arrayOf(schema.string())])
@ -82,30 +82,13 @@ export const getMonitors = async (
sortField,
sortOrder,
query,
tags,
monitorTypes,
locations,
filter = '',
searchAfter,
projects,
schedules,
monitorQueryIds,
showFromAllSpaces,
} = context.request.query;
const { filtersStr } = await getMonitorFilters({
filter,
monitorTypes,
tags,
locations,
projects,
schedules,
monitorQueryIds,
context,
});
const { filtersStr } = await getMonitorFilters(context);
return context.savedObjectsClient.find({
type: syntheticsMonitorType,
return context.monitorConfigRepository.find({
perPage,
page,
sortField: parseMappingKey(sortField),
@ -129,16 +112,26 @@ interface Filters {
monitorQueryIds?: string | string[];
}
export const getMonitorFilters = async (
data: {
context: RouteContext;
} & Filters
) => {
const { context, locations } = data;
export const getMonitorFilters = async (context: RouteContext) => {
const {
tags,
monitorTypes,
locations,
filter = '',
projects,
schedules,
monitorQueryIds,
} = context.request.query;
const locationFilter = await parseLocationFilter(context, locations);
return parseArrayFilters({
...data,
filter,
tags,
monitorTypes,
locations,
projects,
schedules,
monitorQueryIds,
locationFilter,
});
};
@ -260,7 +253,7 @@ export const isMonitorsQueryFiltered = (monitorQuery: MonitorsQuery) => {
);
};
function parseMappingKey(key: string | undefined) {
export function parseMappingKey(key: string | undefined) {
switch (key) {
case 'schedule.keyword':
return 'schedule.number';

View file

@ -6,7 +6,7 @@
*/
import { schema } from '@kbn/config-schema';
import { SyntheticsRestApiRouteFactory } from '../types';
import { syntheticsMonitorType } from '../../../common/types/saved_objects';
import { monitorAttributes, syntheticsMonitorType } from '../../../common/types/saved_objects';
import { ConfigKey, MonitorFiltersResult } from '../../../common/runtime_types';
import { SYNTHETICS_API_URLS } from '../../../common/constants';
@ -87,31 +87,31 @@ export const getSyntheticsFilters: SyntheticsRestApiRouteFactory<MonitorFiltersR
const aggs = {
monitorTypes: {
terms: {
field: `${syntheticsMonitorType}.attributes.${ConfigKey.MONITOR_TYPE}.keyword`,
field: `${monitorAttributes}.${ConfigKey.MONITOR_TYPE}.keyword`,
size: 10000,
},
},
tags: {
terms: {
field: `${syntheticsMonitorType}.attributes.${ConfigKey.TAGS}`,
field: `${monitorAttributes}.${ConfigKey.TAGS}`,
size: 10000,
},
},
locations: {
terms: {
field: `${syntheticsMonitorType}.attributes.${ConfigKey.LOCATIONS}.id`,
field: `${monitorAttributes}.${ConfigKey.LOCATIONS}.id`,
size: 10000,
},
},
projects: {
terms: {
field: `${syntheticsMonitorType}.attributes.${ConfigKey.PROJECT_ID}`,
field: `${monitorAttributes}.${ConfigKey.PROJECT_ID}`,
size: 10000,
},
},
schedules: {
terms: {
field: `${syntheticsMonitorType}.attributes.${ConfigKey.SCHEDULE}.number`,
field: `${monitorAttributes}.${ConfigKey.SCHEDULE}.number`,
size: 10000,
},
},

View file

@ -7,14 +7,13 @@
import { v4 as uuidV4 } from 'uuid';
import { SavedObject } from '@kbn/core-saved-objects-common/src/server_types';
import { SavedObjectsClientContract } from '@kbn/core-saved-objects-api-server';
import { isValidNamespace } from '@kbn/fleet-plugin/common';
import { i18n } from '@kbn/i18n';
import { DeleteMonitorAPI } from '../services/delete_monitor_api';
import { parseMonitorLocations } from './utils';
import { MonitorValidationError } from '../monitor_validation';
import { getSavedObjectKqlFilter } from '../../common';
import { monitorAttributes, syntheticsMonitorType } from '../../../../common/types/saved_objects';
import { monitorAttributes } from '../../../../common/types/saved_objects';
import { PrivateLocationAttributes } from '../../../runtime_types/private_locations';
import { ConfigKey } from '../../../../common/constants/monitor_management';
import {
@ -37,7 +36,6 @@ import { triggerTestNow } from '../../synthetics_service/test_now_monitor';
import { DefaultAlertService } from '../../default_alerts/default_alert_service';
import { RouteContext } from '../../types';
import { formatTelemetryEvent, sendTelemetryEvents } from '../../telemetry/monitor_upgrade_sender';
import { formatSecrets } from '../../../synthetics_service/utils';
import { formatKibanaNamespace } from '../../../../common/formatters';
import { getPrivateLocations } from '../../../synthetics_service/get_private_locations';
@ -63,7 +61,7 @@ export class AddEditMonitorAPI {
id?: string;
normalizedMonitor: SyntheticsMonitor;
}) {
const { savedObjectsClient, server, syntheticsMonitorClient, spaceId } = this.routeContext;
const { server, syntheticsMonitorClient, spaceId } = this.routeContext;
const newMonitorId = id ?? uuidV4();
let monitorSavedObject: SavedObject<EncryptedSyntheticsMonitorAttributes> | null = null;
@ -73,10 +71,9 @@ export class AddEditMonitorAPI {
});
try {
const newMonitorPromise = this.createNewSavedObjectMonitor({
const newMonitorPromise = this.routeContext.monitorConfigRepository.create({
normalizedMonitor: monitorWithNamespace,
id: newMonitorId,
savedObjectsClient,
});
const syncErrorsPromise = syntheticsMonitorClient.addMonitors(
@ -132,32 +129,6 @@ export class AddEditMonitorAPI {
}
}
async createNewSavedObjectMonitor({
id,
savedObjectsClient,
normalizedMonitor,
}: {
id: string;
savedObjectsClient: SavedObjectsClientContract;
normalizedMonitor: SyntheticsMonitor;
}) {
return await savedObjectsClient.create<EncryptedSyntheticsMonitorAttributes>(
syntheticsMonitorType,
formatSecrets({
...normalizedMonitor,
[ConfigKey.MONITOR_QUERY_ID]: normalizedMonitor[ConfigKey.CUSTOM_HEARTBEAT_ID] || id,
[ConfigKey.CONFIG_ID]: id,
revision: 1,
}),
id
? {
id,
overwrite: true,
}
: undefined
);
}
validateMonitorType(monitorFields: MonitorFields, previousMonitor?: MonitorFields) {
const { [ConfigKey.MONITOR_TYPE]: monitorType } = monitorFields;
if (previousMonitor) {
@ -237,11 +208,10 @@ export class AddEditMonitorAPI {
}
async validateUniqueMonitorName(name: string, id?: string) {
const { savedObjectsClient } = this.routeContext;
const { monitorConfigRepository } = this.routeContext;
const kqlFilter = getSavedObjectKqlFilter({ field: 'name.keyword', values: name });
const { total } = await savedObjectsClient.find({
const { total } = await monitorConfigRepository.find({
perPage: 0,
type: syntheticsMonitorType,
filter: id ? `${kqlFilter} and not (${monitorAttributes}.config_id: ${id})` : kqlFilter,
});
@ -330,14 +300,11 @@ export class AddEditMonitorAPI {
}
async revertMonitorIfCreated({ newMonitorId }: { newMonitorId: string }) {
const { server, savedObjectsClient } = this.routeContext;
const { server, monitorConfigRepository } = this.routeContext;
try {
const encryptedMonitor = await savedObjectsClient.get<EncryptedSyntheticsMonitorAttributes>(
syntheticsMonitorType,
newMonitorId
);
const encryptedMonitor = await monitorConfigRepository.get(newMonitorId);
if (encryptedMonitor) {
await savedObjectsClient.delete(syntheticsMonitorType, newMonitorId);
await monitorConfigRepository.delete(newMonitorId);
const deleteMonitorAPI = new DeleteMonitorAPI(this.routeContext);
await deleteMonitorAPI.execute({

View file

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

View file

@ -7,7 +7,6 @@
import { SavedObject, SavedObjectsUpdateResponse } from '@kbn/core/server';
import { SavedObjectError } from '@kbn/core-saved-objects-common';
import { RouteContext } from '../../types';
import { syntheticsMonitorType } from '../../../../common/types/saved_objects';
import { FailedPolicyUpdate } from '../../../synthetics_service/private_location/synthetics_private_location';
import {
ConfigKey,
@ -31,27 +30,6 @@ export interface MonitorConfigUpdate {
decryptedPreviousMonitor: SavedObject<SyntheticsMonitorWithSecretsAttributes>;
}
const updateConfigSavedObjects = async ({
routeContext,
monitorsToUpdate,
}: {
routeContext: RouteContext;
monitorsToUpdate: MonitorConfigUpdate[];
}) => {
return await routeContext.savedObjectsClient.bulkUpdate<MonitorFields>(
monitorsToUpdate.map(({ monitorWithRevision, decryptedPreviousMonitor }) => ({
type: syntheticsMonitorType,
id: decryptedPreviousMonitor.id,
attributes: {
...monitorWithRevision,
[ConfigKey.CONFIG_ID]: decryptedPreviousMonitor.id,
[ConfigKey.MONITOR_QUERY_ID]:
monitorWithRevision[ConfigKey.CUSTOM_HEARTBEAT_ID] || decryptedPreviousMonitor.id,
},
}))
);
};
async function syncUpdatedMonitors({
spaceId,
privateLocations,
@ -92,11 +70,20 @@ export const syncEditedMonitorBulk = async ({
privateLocations: SyntheticsPrivateLocations;
spaceId: string;
}) => {
const { server } = routeContext;
const { server, monitorConfigRepository } = routeContext;
try {
const data = monitorsToUpdate.map(({ monitorWithRevision, decryptedPreviousMonitor }) => ({
id: decryptedPreviousMonitor.id,
attributes: {
...monitorWithRevision,
[ConfigKey.CONFIG_ID]: decryptedPreviousMonitor.id,
[ConfigKey.MONITOR_QUERY_ID]:
monitorWithRevision[ConfigKey.CUSTOM_HEARTBEAT_ID] || decryptedPreviousMonitor.id,
} as unknown as MonitorFields,
}));
const [editedMonitorSavedObjects, editSyncResponse] = await Promise.all([
updateConfigSavedObjects({ monitorsToUpdate, routeContext }),
monitorConfigRepository.bulkUpdate({ monitors: data }),
syncUpdatedMonitors({ monitorsToUpdate, routeContext, spaceId, privateLocations }),
]);
@ -145,15 +132,14 @@ export const rollbackCompletely = async ({
monitorsToUpdate: MonitorConfigUpdate[];
routeContext: RouteContext;
}) => {
const { savedObjectsClient, server } = routeContext;
const { server, monitorConfigRepository } = routeContext;
try {
await savedObjectsClient.bulkUpdate<MonitorFields>(
monitorsToUpdate.map(({ decryptedPreviousMonitor }) => ({
type: syntheticsMonitorType,
await monitorConfigRepository.bulkUpdate({
monitors: monitorsToUpdate.map(({ decryptedPreviousMonitor }) => ({
id: decryptedPreviousMonitor.id,
attributes: decryptedPreviousMonitor.attributes,
}))
);
attributes: decryptedPreviousMonitor.attributes as unknown as MonitorFields,
})),
});
} catch (e) {
server.logger.error(`Unable to rollback Synthetics monitors edit ${e.message} `);
}
@ -173,7 +159,7 @@ export const rollbackFailedUpdates = async ({
if (!failedPolicyUpdates || failedPolicyUpdates.length === 0) {
return;
}
const { server, savedObjectsClient } = routeContext;
const { server, monitorConfigRepository } = routeContext;
try {
const failedConfigs: Record<
@ -195,13 +181,12 @@ export const rollbackFailedUpdates = async ({
return failedConfigs[decryptedPreviousMonitor.id];
})
.map(({ decryptedPreviousMonitor }) => ({
type: syntheticsMonitorType,
id: decryptedPreviousMonitor.id,
attributes: decryptedPreviousMonitor.attributes,
attributes: decryptedPreviousMonitor.attributes as unknown as MonitorFields,
}));
if (monitorsToRevert.length > 0) {
await savedObjectsClient.bulkUpdate<MonitorFields>(monitorsToRevert);
await monitorConfigRepository.bulkUpdate({ monitors: monitorsToRevert });
}
return failedConfigs;
} catch (e) {

View file

@ -13,7 +13,6 @@ import { ConfigKey, EncryptedSyntheticsMonitorAttributes } from '../../../common
import { SYNTHETICS_API_URLS } from '../../../common/constants';
import { getMonitorNotFoundResponse } from '../synthetics_service/service_errors';
import { mapSavedObjectToMonitor } from './formatters/saved_object_to_monitor';
import { getSyntheticsMonitor } from '../../queries/get_monitor';
export const getSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () => ({
method: 'GET',
@ -39,6 +38,7 @@ export const getSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () => ({
server: { encryptedSavedObjects, coreStart },
savedObjectsClient,
spaceId,
monitorConfigRepository,
}): Promise<any> => {
const { monitorId } = request.params;
try {
@ -53,13 +53,7 @@ export const getSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () => ({
if (Boolean(canSave)) {
// only user with write permissions can decrypt the monitor
const encryptedSavedObjectsClient = encryptedSavedObjects.getClient();
const monitor = await getSyntheticsMonitor({
monitorId,
encryptedSavedObjectsClient,
spaceId,
});
const monitor = await monitorConfigRepository.getDecrypted(monitorId, spaceId);
return { ...mapSavedObjectToMonitor({ monitor, internal }), spaceId };
} else {
return {

View file

@ -8,7 +8,6 @@ import { mapSavedObjectToMonitor } from './formatters/saved_object_to_monitor';
import { SyntheticsRestApiRouteFactory } from '../types';
import { SYNTHETICS_API_URLS } from '../../../common/constants';
import { getMonitors, isMonitorsQueryFiltered, QuerySchema } from '../common';
import { syntheticsMonitorType } from '../../../common/types/saved_objects';
export const getAllSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () => ({
method: 'GET',
@ -20,11 +19,10 @@ export const getAllSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () =>
},
},
handler: async (routeContext): Promise<any> => {
const { request, savedObjectsClient, syntheticsMonitorClient } = routeContext;
const { request, syntheticsMonitorClient, monitorConfigRepository } = routeContext;
const totalCountQuery = async () => {
if (isMonitorsQueryFiltered(request.query)) {
return savedObjectsClient.find({
type: syntheticsMonitorType,
return monitorConfigRepository.find({
perPage: 0,
page: 1,
});

View file

@ -65,13 +65,10 @@ export const addSyntheticsProjectMonitorRoute: SyntheticsRestApiRouteFactory = (
return response.forbidden({ body: { message: permissionError } });
}
const encryptedSavedObjectsClient = server.encryptedSavedObjects.getClient();
const pushMonitorFormatter = new ProjectMonitorFormatter({
routeContext,
projectId: decodedProjectName,
spaceId,
encryptedSavedObjectsClient,
monitors,
});

View file

@ -8,7 +8,7 @@ import { schema } from '@kbn/config-schema';
import { i18n } from '@kbn/i18n';
import { DeleteMonitorAPI } from '../services/delete_monitor_api';
import { SyntheticsRestApiRouteFactory } from '../../types';
import { syntheticsMonitorType } from '../../../../common/types/saved_objects';
import { monitorAttributes } from '../../../../common/types/saved_objects';
import { ConfigKey } from '../../../../common/runtime_types';
import { SYNTHETICS_API_URLS } from '../../../../common/constants';
import { getMonitors, getSavedObjectKqlFilter } from '../../common';
@ -40,7 +40,7 @@ export const deleteSyntheticsMonitorProjectRoute: SyntheticsRestApiRouteFactory
await validateSpaceId(routeContext);
const deleteFilter = `${syntheticsMonitorType}.attributes.${
const deleteFilter = `${monitorAttributes}.${
ConfigKey.PROJECT_ID
}: "${decodedProjectName}" AND ${getSavedObjectKqlFilter({
field: 'journey_id',

View file

@ -130,7 +130,7 @@ export class DeleteMonitorAPI {
}: {
monitors: Array<SavedObject<SyntheticsMonitor | EncryptedSyntheticsMonitorAttributes>>;
}) {
const { savedObjectsClient, server, spaceId, syntheticsMonitorClient } = this.routeContext;
const { server, spaceId, syntheticsMonitorClient } = this.routeContext;
const { logger, telemetry, stackVersion } = server;
try {
@ -139,15 +139,14 @@ export class DeleteMonitorAPI {
...normalizedMonitor.attributes,
id: normalizedMonitor.attributes[ConfigKey.MONITOR_QUERY_ID],
})) as SyntheticsMonitorWithId[],
savedObjectsClient,
spaceId
);
const deletePromises = savedObjectsClient.bulkDelete(
monitors.map((monitor) => ({ type: syntheticsMonitorType, id: monitor.id }))
const deletePromise = this.routeContext.monitorConfigRepository.bulkDelete(
monitors.map((monitor) => monitor.id)
);
const [errors, result] = await Promise.all([deleteSyncPromise, deletePromises]);
const [errors, result] = await Promise.all([deleteSyncPromise, deletePromise]);
monitors.forEach((monitor) => {
sendTelemetryEvents(

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';
@ -158,7 +157,7 @@ describe('current status route', () => {
})
);
const routeContext: any = {
request: {},
request: { query: {} },
syntheticsEsClient,
};
@ -316,7 +315,7 @@ describe('current status route', () => {
);
const routeContext: any = {
request: {},
request: { query: {} },
syntheticsEsClient,
};
@ -420,7 +419,7 @@ describe('current status route', () => {
})
);
const routeContext: any = {
request: {},
request: { query: {} },
syntheticsEsClient,
};
@ -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 {
@ -47,13 +44,7 @@ export class OverviewStatusService {
) {}
async getOverviewStatus() {
const { request } = this.routeContext;
const queryParams = request.query as OverviewStatusQuery;
this.filterData = await getMonitorFilters({
...queryParams,
context: this.routeContext,
});
this.filterData = await getMonitorFilters(this.routeContext);
const [allConfigs, statusResult] = await Promise.all([
this.getMonitorConfigs(),
@ -312,7 +303,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.
@ -323,8 +314,7 @@ export class OverviewStatusService {
const { filtersStr } = this.filterData;
return await getAllMonitors({
soClient: savedObjectsClient,
return await this.routeContext.monitorConfigRepository.getAll({
showFromAllSpaces,
search: query ? `${query}*` : '',
filter: filtersStr,

View file

@ -5,7 +5,6 @@
* 2.0.
*/
import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common';
import { SyntheticsRestApiRouteFactory } from '../types';
import { getPrivateLocations } from '../../synthetics_service/get_private_locations';
import { SYNTHETICS_API_URLS } from '../../../common/constants';
@ -17,16 +16,15 @@ export const syncParamsSyntheticsParamsRoute: SyntheticsRestApiRouteFactory = ()
handler: async ({
savedObjectsClient,
syntheticsMonitorClient,
request,
spaceId,
server,
}): Promise<any> => {
const spaceId = server.spaces?.spacesService.getSpaceId(request) ?? DEFAULT_SPACE_ID;
const allPrivateLocations = await getPrivateLocations(savedObjectsClient);
await syntheticsMonitorClient.syncGlobalParams({
spaceId,
allPrivateLocations,
soClient: savedObjectsClient,
encryptedSavedObjects: server.encryptedSavedObjects,
});

View file

@ -62,22 +62,15 @@ export const getSyntheticsSuggestionsRoute: SyntheticsRestApiRouteFactory<
},
handler: async (route): Promise<any> => {
const {
savedObjectsClient,
server: { logger },
monitorConfigRepository,
} = route;
const { tags, locations, projects, monitorQueryIds, query } = route.request.query;
const { query } = route.request.query;
const { filtersStr } = await getMonitorFilters({
tags,
locations,
projects,
monitorQueryIds,
context: route,
});
const { filtersStr } = await getMonitorFilters(route);
const { allLocations = [] } = await getAllLocations(route);
try {
const data = await savedObjectsClient.find<EncryptedSyntheticsMonitorAttributes>({
type: syntheticsMonitorType,
const data = await monitorConfigRepository.find<EncryptedSyntheticsMonitorAttributes>({
perPage: 0,
filter: filtersStr ? `${filtersStr}` : undefined,
aggs,

View file

@ -61,7 +61,6 @@ export const runOnceSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () =
id: monitorId,
testRunId: monitorId,
},
savedObjectsClient,
privateLocations,
spaceId,
true

View file

@ -58,7 +58,6 @@ export const triggerTestNow = async (
id: monitorId,
testRunId,
},
savedObjectsClient,
privateLocations,
spaceId
);

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';
@ -84,16 +85,6 @@ export type SyntheticsRouteWrapper = (
syntheticsMonitorClient: SyntheticsMonitorClient
) => UMKibanaRoute;
export interface UptimeRouteContext {
syntheticsEsClient: SyntheticsEsClient;
context: UptimeRequestHandlerContext;
request: SyntheticsRequest;
response: KibanaResponseFactory;
savedObjectsClient: SavedObjectsClientContract;
server: SyntheticsServerSetup;
subject?: Subject<unknown>;
}
export interface RouteContext<
Params = Record<string, any>,
Query = Record<string, any>,
@ -108,6 +99,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,176 @@
/*
* 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({
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<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;
});
}
}

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

@ -27,6 +27,7 @@ import { formatLocation } from '../../../common/utils/location_formatter';
import * as locationsUtil from '../get_all_locations';
import { mockEncryptedSO } from '../utils/mocks';
import { SyntheticsServerSetup } from '../../types';
import { MonitorConfigRepository } from '../../services/monitor_config_repository';
const testMonitors = [
{
@ -153,6 +154,7 @@ describe('ProjectMonitorFormatter', () => {
server: serverMock,
syntheticsMonitorClient: monitorClient,
request: kibanaRequest,
monitorConfigRepository: new MonitorConfigRepository(soClient, encryptedSavedObjectsClient),
} as any;
jest.spyOn(locationsUtil, 'getAllLocations').mockImplementation(
@ -203,7 +205,6 @@ describe('ProjectMonitorFormatter', () => {
projectId: 'test-project',
spaceId: 'default',
routeContext,
encryptedSavedObjectsClient,
monitors: [invalidMonitor],
});
@ -239,7 +240,6 @@ describe('ProjectMonitorFormatter', () => {
const pushMonitorFormatter = new ProjectMonitorFormatter({
projectId: 'test-project',
spaceId: 'default-space',
encryptedSavedObjectsClient,
monitors: testMonitors,
routeContext,
});
@ -271,7 +271,6 @@ describe('ProjectMonitorFormatter', () => {
const pushMonitorFormatter = new ProjectMonitorFormatter({
projectId: 'test-project',
spaceId: 'default-space',
encryptedSavedObjectsClient,
monitors: testMonitors,
routeContext,
});
@ -303,7 +302,6 @@ describe('ProjectMonitorFormatter', () => {
const pushMonitorFormatter = new ProjectMonitorFormatter({
projectId: 'test-project',
spaceId: 'default-space',
encryptedSavedObjectsClient,
monitors: testMonitors,
routeContext,
});
@ -341,7 +339,6 @@ describe('ProjectMonitorFormatter', () => {
const pushMonitorFormatter = new ProjectMonitorFormatter({
projectId: 'test-project',
spaceId: 'default-space',
encryptedSavedObjectsClient,
monitors: testMonitors,
routeContext,
});

View file

@ -4,13 +4,8 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import {
SavedObjectsUpdateResponse,
SavedObjectsClientContract,
SavedObjectsFindResult,
} from '@kbn/core/server';
import { SavedObjectsUpdateResponse, SavedObjectsClientContract } from '@kbn/core/server';
import { i18n } from '@kbn/i18n';
import { EncryptedSavedObjectsClient } from '@kbn/encrypted-saved-objects-plugin/server';
import { getSavedObjectKqlFilter } from '../../routes/common';
import { InvalidLocationError } from './normalizers/common_fields';
import { SyntheticsServerSetup } from '../../types';
@ -25,7 +20,6 @@ import {
} from '../../routes/monitor_cruds/bulk_cruds/edit_monitor_bulk';
import {
ConfigKey,
SyntheticsMonitorWithSecretsAttributes,
EncryptedSyntheticsMonitorAttributes,
ServiceLocationErrors,
ProjectMonitor,
@ -76,7 +70,6 @@ export class ProjectMonitorFormatter {
private publicLocations: Locations;
private privateLocations: SyntheticsPrivateLocations;
private savedObjectsClient: SavedObjectsClientContract;
private encryptedSavedObjectsClient: EncryptedSavedObjectsClient;
private monitors: ProjectMonitor[] = [];
public createdMonitors: string[] = [];
public updatedMonitors: string[] = [];
@ -87,14 +80,12 @@ export class ProjectMonitorFormatter {
private routeContext: RouteContext;
constructor({
encryptedSavedObjectsClient,
projectId,
spaceId,
monitors,
routeContext,
}: {
routeContext: RouteContext;
encryptedSavedObjectsClient: EncryptedSavedObjectsClient;
projectId: string;
spaceId: string;
monitors: ProjectMonitor[];
@ -103,7 +94,6 @@ export class ProjectMonitorFormatter {
this.projectId = projectId;
this.spaceId = spaceId;
this.savedObjectsClient = routeContext.savedObjectsClient;
this.encryptedSavedObjectsClient = encryptedSavedObjectsClient;
this.syntheticsMonitorClient = routeContext.syntheticsMonitorClient;
this.monitors = monitors;
this.server = routeContext.server;
@ -262,9 +252,8 @@ export class ProjectMonitorFormatter {
field: ConfigKey.JOURNEY_ID,
values: journeyIds,
});
const finder = this.savedObjectsClient.createPointInTimeFinder<ExistingMonitor>({
type: syntheticsMonitorType,
perPage: 5000,
const result = await this.routeContext.monitorConfigRepository.find<ExistingMonitor>({
filter: `${this.projectFilter} AND ${journeyFilter}`,
fields: [
ConfigKey.JOURNEY_ID,
@ -274,21 +263,12 @@ export class ProjectMonitorFormatter {
],
});
const hits: PreviousMonitorForUpdate[] = [];
for await (const result of finder.find()) {
hits.push(
...result.saved_objects.map((monitor) => {
return {
...monitor.attributes,
updated_at: monitor.updated_at,
};
})
);
}
finder.close().catch(() => {});
return hits;
return result.saved_objects.map<PreviousMonitorForUpdate>((monitor) => {
return {
...monitor.attributes,
updated_at: monitor.updated_at,
};
});
};
private createMonitorsBulk = async (monitors: SyntheticsMonitor[]) => {
@ -363,25 +343,11 @@ export class ProjectMonitorFormatter {
field: ConfigKey.CONFIG_ID,
values: configIds,
});
const finder =
await this.encryptedSavedObjectsClient.createPointInTimeFinderDecryptedAsInternalUser<SyntheticsMonitorWithSecretsAttributes>(
{
filter: monitorFilter,
type: syntheticsMonitorType,
perPage: 500,
namespaces: [this.spaceId],
}
);
const decryptedMonitors: Array<SavedObjectsFindResult<SyntheticsMonitorWithSecretsAttributes>> =
[];
for await (const result of finder.find()) {
decryptedMonitors.push(...result.saved_objects);
}
finder.close().catch(() => {});
return decryptedMonitors;
return await this.routeContext.monitorConfigRepository.findDecryptedMonitors({
filter: monitorFilter,
spaceId: this.spaceId,
});
};
private updateMonitorsBulk = async (

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { loggerMock } from '@kbn/logging-mocks';
import { SavedObjectsClientContract, CoreStart } from '@kbn/core/server';
import { CoreStart } from '@kbn/core/server';
import { coreMock } from '@kbn/core/server/mocks';
import { SyntheticsMonitorClient } from './synthetics_monitor_client';
import { SyntheticsService } from '../synthetics_service';
@ -42,10 +42,6 @@ describe('SyntheticsMonitorClient', () => {
const mockEsClient = {
search: jest.fn(),
};
const savedObjectsClientMock = {
bulkUpdate: jest.fn(),
get: jest.fn(),
} as unknown as SavedObjectsClientContract;
const logger = loggerMock.create();
@ -204,11 +200,7 @@ describe('SyntheticsMonitorClient', () => {
client.privateLocationAPI.deleteMonitors = jest.fn();
syntheticsService.deleteConfigs = jest.fn();
await client.deleteMonitors(
[monitor as unknown as SyntheticsMonitorWithId],
savedObjectsClientMock,
'test-space'
);
await client.deleteMonitors([monitor as unknown as SyntheticsMonitorWithId], 'test-space');
expect(syntheticsService.deleteConfigs).toHaveBeenCalledTimes(1);
expect(client.privateLocationAPI.deleteMonitors).toHaveBeenCalledTimes(1);

View file

@ -6,8 +6,8 @@
*/
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';
import {
PrivateConfig,
@ -162,11 +162,7 @@ export class SyntheticsMonitorClient {
return { failedPolicyUpdates, publicSyncErrors };
}
async deleteMonitors(
monitors: SyntheticsMonitorWithId[],
savedObjectsClient: SavedObjectsClientContract,
spaceId: string
) {
async deleteMonitors(monitors: SyntheticsMonitorWithId[], spaceId: string) {
const privateDeletePromise = this.privateLocationAPI.deleteMonitors(monitors, spaceId);
const publicDeletePromise = this.syntheticsService.deleteConfigs(
@ -179,7 +175,6 @@ export class SyntheticsMonitorClient {
async testNowConfigs(
monitor: { monitor: MonitorFields; id: string; testRunId: string },
savedObjectsClient: SavedObjectsClientContract,
allPrivateLocations: PrivateLocationAttributes[],
spaceId: string,
runOnce?: true
@ -274,8 +269,10 @@ export class SyntheticsMonitorClient {
spaceId,
allPrivateLocations,
encryptedSavedObjects,
soClient,
}: {
spaceId: string;
soClient: SavedObjectsClientContract;
allPrivateLocations: PrivateLocationAttributes[];
encryptedSavedObjects: EncryptedSavedObjectsPluginStart;
}) {
@ -285,6 +282,7 @@ export class SyntheticsMonitorClient {
const { allConfigs: monitors, paramsBySpace } = await this.getAllMonitorConfigs({
encryptedSavedObjects,
soClient,
spaceId,
});
@ -309,14 +307,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]);
@ -326,37 +330,6 @@ export class SyntheticsMonitorClient {
};
}
async getAllMonitors({
spaceId,
encryptedSavedObjects,
}: {
spaceId: string;
encryptedSavedObjects: EncryptedSavedObjectsPluginStart;
}) {
const encryptedClient = encryptedSavedObjects.getClient();
const monitors: Array<SavedObjectsFindResult<SyntheticsMonitorWithSecretsAttributes>> = [];
const finder =
await encryptedClient.createPointInTimeFinderDecryptedAsInternalUser<SyntheticsMonitorWithSecretsAttributes>(
{
type: syntheticsMonitorType,
perPage: 1000,
namespaces: [spaceId],
}
);
for await (const response of finder.find()) {
response.saved_objects.forEach((monitor) => {
monitors.push(monitor);
});
}
finder.close().catch(() => {});
return monitors;
}
mixParamsWithMonitors(
spaceId: string,
monitors: Array<SavedObjectsFindResult<SyntheticsMonitorWithSecretsAttributes>>,

View file

@ -29,11 +29,10 @@ export function normalizeSecrets(
monitor: SavedObject<SyntheticsMonitorWithSecretsAttributes | SyntheticsMonitor880>
): SavedObject<SyntheticsMonitor> {
const attributes = normalizeMonitorSecretAttributes(monitor.attributes);
const normalizedMonitor = {
return {
...monitor,
attributes,
};
return normalizedMonitor;
}
export function normalizeMonitorSecretAttributes(