[Synthetics] Add error handling for private locations (#137115)

* add error handling for private locations

* add jest tests

* add tests

* adjust types

* adjust tests

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Dominique Clarke 2022-07-26 16:34:56 -04:00 committed by GitHub
parent cc31f24af5
commit 0fbfa50075
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 673 additions and 145 deletions

View file

@ -5,7 +5,12 @@
* 2.0.
*/
import { schema } from '@kbn/config-schema';
import { SavedObject, SavedObjectsErrorHelpers } from '@kbn/core/server';
import {
SavedObject,
SavedObjectsErrorHelpers,
SavedObjectsClientContract,
KibanaRequest,
} from '@kbn/core/server';
import { SyntheticsMonitorClient } from '../../synthetics_service/synthetics_monitor/synthetics_monitor_client';
import {
ConfigKey,
@ -21,6 +26,7 @@ import { validateMonitor } from './monitor_validation';
import { sendTelemetryEvents, formatTelemetryEvent } from '../telemetry/monitor_upgrade_sender';
import { formatSecrets } from '../../synthetics_service/utils/secrets';
import type { UptimeServerSetup } from '../../legacy_uptime/lib/adapters/framework';
import { deleteMonitor } from './delete_monitor';
export const addSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () => ({
method: 'POST',
@ -89,6 +95,8 @@ export const addSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () => ({
monitorSavedObject: newMonitor,
server,
syntheticsMonitorClient,
savedObjectsClient,
request,
});
if (errors && errors.length > 0) {
@ -110,27 +118,43 @@ export const syncNewMonitor = async ({
monitorSavedObject,
server,
syntheticsMonitorClient,
savedObjectsClient,
request,
}: {
monitor: SyntheticsMonitor;
monitorSavedObject: SavedObject<EncryptedSyntheticsMonitor>;
server: UptimeServerSetup;
syntheticsMonitorClient: SyntheticsMonitorClient;
savedObjectsClient: SavedObjectsClientContract;
request: KibanaRequest;
}) => {
const errors = await syntheticsMonitorClient.addMonitor(
monitor as MonitorFields,
monitorSavedObject.id
);
try {
const errors = await syntheticsMonitorClient.addMonitor(
monitor as MonitorFields,
monitorSavedObject.id,
request
);
sendTelemetryEvents(
server.logger,
server.telemetry,
formatTelemetryEvent({
monitor: monitorSavedObject,
errors,
isInlineScript: Boolean((monitor as MonitorFields)[ConfigKey.SOURCE_INLINE]),
kibanaVersion: server.kibanaVersion,
})
);
sendTelemetryEvents(
server.logger,
server.telemetry,
formatTelemetryEvent({
monitor: monitorSavedObject,
errors,
isInlineScript: Boolean((monitor as MonitorFields)[ConfigKey.SOURCE_INLINE]),
kibanaVersion: server.kibanaVersion,
})
);
return errors;
return errors;
} catch (e) {
await deleteMonitor({
savedObjectsClient,
server,
monitorId: monitorSavedObject.id,
syntheticsMonitorClient,
request,
});
throw e;
}
};

View file

@ -5,7 +5,11 @@
* 2.0.
*/
import { schema } from '@kbn/config-schema';
import { SavedObjectsClientContract, SavedObjectsErrorHelpers } from '@kbn/core/server';
import {
SavedObjectsClientContract,
SavedObjectsErrorHelpers,
KibanaRequest,
} from '@kbn/core/server';
import { SyntheticsMonitorClient } from '../../synthetics_service/synthetics_monitor/synthetics_monitor_client';
import {
ConfigKey,
@ -50,6 +54,7 @@ export const deleteSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () =>
server,
monitorId,
syntheticsMonitorClient,
request,
});
if (errors && errors.length > 0) {
@ -74,11 +79,13 @@ export const deleteMonitor = async ({
server,
monitorId,
syntheticsMonitorClient,
request,
}: {
savedObjectsClient: SavedObjectsClientContract;
server: UptimeServerSetup;
monitorId: string;
syntheticsMonitorClient: SyntheticsMonitorClient;
request: KibanaRequest;
}) => {
const { logger, telemetry, kibanaVersion, encryptedSavedObjects } = server;
const encryptedSavedObjectsClient = encryptedSavedObjects.getClient();
@ -99,12 +106,16 @@ export const deleteMonitor = async ({
const normalizedMonitor = normalizeSecrets(monitor);
const errors = await syntheticsMonitorClient.deleteMonitor(
{
...normalizedMonitor.attributes,
id:
(normalizedMonitor.attributes as MonitorFields)[ConfigKey.CUSTOM_HEARTBEAT_ID] ||
monitorId,
},
request
);
await savedObjectsClient.delete(syntheticsMonitorType, monitorId);
const errors = await syntheticsMonitorClient.deleteMonitor({
...normalizedMonitor.attributes,
id:
(normalizedMonitor.attributes as MonitorFields)[ConfigKey.CUSTOM_HEARTBEAT_ID] || monitorId,
});
sendTelemetryEvents(
logger,

View file

@ -7,8 +7,17 @@
import { loggerMock } from '@kbn/logging-mocks';
import { syncEditedMonitor } from './edit_monitor';
import { SavedObjectsUpdateResponse, SavedObject } from '@kbn/core/server';
import { EncryptedSyntheticsMonitor, SyntheticsMonitor } from '../../../common/runtime_types';
import {
SavedObjectsUpdateResponse,
SavedObject,
SavedObjectsClientContract,
KibanaRequest,
} from '@kbn/core/server';
import {
EncryptedSyntheticsMonitor,
SyntheticsMonitor,
SyntheticsMonitorWithSecrets,
} from '../../../common/runtime_types';
import { UptimeServerSetup } from '../../legacy_uptime/lib/adapters';
import { SyntheticsService } from '../../synthetics_service/synthetics_service';
import { SyntheticsMonitorClient } from '../../synthetics_service/synthetics_monitor/synthetics_monitor_client';
@ -24,7 +33,7 @@ describe('syncEditedMonitor', () => {
const serverMock: UptimeServerSetup = {
uptimeEsClient: { search: jest.fn() },
kibanaVersion: null,
authSavedObjectsClient: { bulkUpdate: jest.fn(), get: jest.fn() },
authSavedObjectsClient: { bulkUpdate: jest.fn(), get: jest.fn(), update: jest.fn() },
logger,
config: {
service: {
@ -32,6 +41,13 @@ describe('syncEditedMonitor', () => {
password: '12345',
},
},
fleet: {
authz: {
fromRequest: jest
.fn()
.mockReturnValue({ integrations: { writeIntegrationPolicies: true } }),
},
},
} as unknown as UptimeServerSetup;
const editedMonitor = {
@ -57,7 +73,10 @@ describe('syncEditedMonitor', () => {
fields_under_root: true,
} as unknown as SyntheticsMonitor;
const previousMonitor = { id: 'saved-obj-id' } as SavedObject<EncryptedSyntheticsMonitor>;
const previousMonitor = {
id: 'saved-obj-id',
attributes: { name: editedMonitor.name },
} as SavedObject<EncryptedSyntheticsMonitor>;
const editedMonitorSavedObject = {
id: 'saved-obj-id',
} as SavedObjectsUpdateResponse<EncryptedSyntheticsMonitor>;
@ -73,8 +92,13 @@ describe('syncEditedMonitor', () => {
editedMonitor,
editedMonitorSavedObject,
previousMonitor,
decryptedPreviousMonitor:
previousMonitor as unknown as SavedObject<SyntheticsMonitorWithSecrets>,
syntheticsMonitorClient,
server: serverMock,
request: {} as unknown as KibanaRequest,
savedObjectsClient:
serverMock.authSavedObjectsClient as unknown as SavedObjectsClientContract,
});
expect(syntheticsService.editConfig).toHaveBeenCalledWith(

View file

@ -6,7 +6,12 @@
*/
import { schema } from '@kbn/config-schema';
import { SavedObjectsUpdateResponse, SavedObject } from '@kbn/core/server';
import {
SavedObjectsUpdateResponse,
SavedObject,
SavedObjectsClientContract,
KibanaRequest,
} from '@kbn/core/server';
import { SavedObjectsErrorHelpers } from '@kbn/core/server';
import { SyntheticsMonitorClient } from '../../synthetics_service/synthetics_monitor/synthetics_monitor_client';
import {
@ -70,9 +75,10 @@ export const editSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () => (
namespace: previousMonitor.namespaces?.[0],
}
);
const normalizedPreviousMonitor = normalizeSecrets(decryptedPreviousMonitor).attributes;
const editedMonitor = {
...normalizeSecrets(decryptedPreviousMonitor).attributes,
...normalizedPreviousMonitor,
...monitor,
};
@ -101,7 +107,10 @@ export const editSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () => (
editedMonitor,
editedMonitorSavedObject,
previousMonitor,
decryptedPreviousMonitor,
syntheticsMonitorClient,
savedObjectsClient,
request,
});
// Return service sync errors in OK response
@ -127,31 +136,51 @@ export const syncEditedMonitor = async ({
editedMonitor,
editedMonitorSavedObject,
previousMonitor,
decryptedPreviousMonitor,
server,
syntheticsMonitorClient,
savedObjectsClient,
request,
}: {
editedMonitor: SyntheticsMonitor;
editedMonitorSavedObject: SavedObjectsUpdateResponse<EncryptedSyntheticsMonitor>;
previousMonitor: SavedObject<EncryptedSyntheticsMonitor>;
decryptedPreviousMonitor: SavedObject<SyntheticsMonitorWithSecrets>;
server: UptimeServerSetup;
syntheticsMonitorClient: SyntheticsMonitorClient;
savedObjectsClient: SavedObjectsClientContract;
request: KibanaRequest;
}) => {
const errors = await syntheticsMonitorClient.editMonitor(
editedMonitor as MonitorFields,
editedMonitorSavedObject.id
);
try {
const errors = await syntheticsMonitorClient.editMonitor(
editedMonitor as MonitorFields,
editedMonitorSavedObject.id,
request
);
sendTelemetryEvents(
server.logger,
server.telemetry,
formatTelemetryUpdateEvent(
editedMonitorSavedObject,
previousMonitor,
server.kibanaVersion,
Boolean((editedMonitor as MonitorFields)[ConfigKey.SOURCE_INLINE]),
errors
)
);
sendTelemetryEvents(
server.logger,
server.telemetry,
formatTelemetryUpdateEvent(
editedMonitorSavedObject,
previousMonitor,
server.kibanaVersion,
Boolean((editedMonitor as MonitorFields)[ConfigKey.SOURCE_INLINE]),
errors
)
);
return errors;
return errors;
} catch (e) {
server.logger.error(
`Unable to update Synthetics monitor ${decryptedPreviousMonitor.attributes[ConfigKey.NAME]}`
);
await savedObjectsClient.update<MonitorFields>(
syntheticsMonitorType,
editedMonitorSavedObject.id,
decryptedPreviousMonitor.attributes
);
throw e;
}
};

View file

@ -4,12 +4,162 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { testMonitorPolicy } from './test_policy';
import { KibanaRequest } from '@kbn/core/server';
import { loggerMock } from '@kbn/logging-mocks';
import { UptimeServerSetup } from '../../legacy_uptime/lib/adapters';
import { formatSyntheticsPolicy } from '../../../common/formatters/format_synthetics_policy';
import { DataStream, MonitorFields, ScheduleUnit, SourceType } from '../../../common/runtime_types';
import {
DataStream,
MonitorFields,
ScheduleUnit,
SourceType,
HeartbeatConfig,
} from '../../../common/runtime_types';
import { SyntheticsPrivateLocation } from './synthetics_private_location';
import { testMonitorPolicy } from './test_policy';
describe('SyntheticsPrivateLocation', () => {
const mockPrivateLocation = {
id: 'policyId',
label: 'Test Location',
isServiceManaged: false,
};
const testConfig = {
id: 'testId',
type: 'http',
enabled: true,
schedule: '@every 3m',
'service.name': '',
locations: [mockPrivateLocation],
tags: [],
timeout: '16',
name: 'Test Monitor',
urls: 'https://www.google.com',
max_redirects: '0',
password: '12345678',
proxy_url: '',
'check.response.body.negative': [],
'check.response.body.positive': [],
'response.include_body': 'on_error',
'check.response.headers': {},
'response.include_headers': true,
'check.response.status': [],
'check.request.body': { type: 'text', value: '' },
'check.request.headers': {},
'check.request.method': 'GET',
username: '',
} as unknown as HeartbeatConfig;
const serverMock: UptimeServerSetup = {
uptimeEsClient: { search: jest.fn() },
logger: loggerMock.create(),
authSavedObjectsClient: {
bulkUpdate: jest.fn(),
get: jest.fn().mockReturnValue({
attributes: {
locations: [mockPrivateLocation],
},
}),
},
config: {
service: {
username: 'dev',
password: '12345',
manifestUrl: 'http://localhost:8080/api/manifest',
},
},
fleet: {
authz: {
fromRequest: jest
.fn()
.mockReturnValue({ integrations: { writeIntegrationPolicies: true } }),
},
packagePolicyService: {
get: jest.fn().mockReturnValue({}),
},
},
} as unknown as UptimeServerSetup;
it.each([
[
true,
'Unable to create Synthetics package policy for monitor Test Monitor with private location Test Location',
],
[
false,
'Unable to create Synthetics package policy for monitor Test Monitor. Fleet write permissions are needed to use Synthetics private locations.',
],
])('throws errors for create monitor', async (writeIntegrationPolicies, error) => {
const syntheticsPrivateLocation = new SyntheticsPrivateLocation({
...serverMock,
fleet: {
...serverMock.fleet,
authz: {
fromRequest: jest.fn().mockReturnValue({ integrations: { writeIntegrationPolicies } }),
},
},
});
try {
await syntheticsPrivateLocation.createMonitor(testConfig, {} as unknown as KibanaRequest);
} catch (e) {
expect(e).toEqual(new Error(error));
}
});
it.each([
[
true,
'Unable to update Synthetics package policy for monitor Test Monitor with private location Test Location',
],
[
false,
'Unable to update Synthetics package policy for monitor Test Monitor. Fleet write permissions are needed to use Synthetics private locations.',
],
])('throws errors for edit monitor', async (writeIntegrationPolicies, error) => {
const syntheticsPrivateLocation = new SyntheticsPrivateLocation({
...serverMock,
fleet: {
...serverMock.fleet,
authz: {
fromRequest: jest.fn().mockReturnValue({ integrations: { writeIntegrationPolicies } }),
},
},
});
try {
await syntheticsPrivateLocation.editMonitor(testConfig, {} as unknown as KibanaRequest);
} catch (e) {
expect(e).toEqual(new Error(error));
}
});
it.each([
[
true,
'Unable to delete Synthetics package policy for monitor Test Monitor with private location Test Location',
],
[
false,
'Unable to delete Synthetics package policy for monitor Test Monitor. Fleet write permissions are needed to use Synthetics private locations.',
],
])('throws errors for delete monitor', async (writeIntegrationPolicies, error) => {
const syntheticsPrivateLocation = new SyntheticsPrivateLocation({
...serverMock,
fleet: {
...serverMock.fleet,
authz: {
fromRequest: jest.fn().mockReturnValue({ integrations: { writeIntegrationPolicies } }),
},
},
});
try {
await syntheticsPrivateLocation.deleteMonitor(testConfig, {} as unknown as KibanaRequest);
} catch (e) {
expect(e).toEqual(new Error(e));
}
});
it('formats monitors stream properly', () => {
const test = formatSyntheticsPolicy(testMonitorPolicy, DataStream.BROWSER, dummyBrowserConfig);

View file

@ -4,8 +4,8 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { NewPackagePolicy, PACKAGE_POLICY_SAVED_OBJECT_TYPE } from '@kbn/fleet-plugin/common';
import { KibanaRequest } from '@kbn/core/server';
import { NewPackagePolicy } from '@kbn/fleet-plugin/common';
import { formatSyntheticsPolicy } from '../../../common/formatters/format_synthetics_policy';
import { getSyntheticsPrivateLocations } from '../../legacy_uptime/lib/saved_objects/private_locations';
import {
@ -58,7 +58,9 @@ export class SyntheticsPrivateLocation {
);
if (!newPolicy) {
throw new Error('Could not create new synthetics policy');
throw new Error(
`Unable to create Synthetics package policy for private location ${privateLocation.label}`
);
}
newPolicy.is_managed = true;
@ -87,33 +89,72 @@ export class SyntheticsPrivateLocation {
}
}
async createMonitor(config: HeartbeatConfig) {
try {
const { locations } = config;
async checkPermissions(request: KibanaRequest, error: string) {
const {
integrations: { writeIntegrationPolicies },
} = await this.server.fleet.authz.fromRequest(request);
const privateLocations: PrivateLocation[] = await getSyntheticsPrivateLocations(
this.server.authSavedObjectsClient!
);
const fleetManagedLocations = locations.filter((loc) => !loc.isServiceManaged);
for (const privateLocation of fleetManagedLocations) {
const location = privateLocations?.find((loc) => loc.id === privateLocation.id)!;
const newPolicy = await this.generateNewPolicy(config, location);
if (!newPolicy) {
throw new Error('Unable to create Synthetics package policy for private location');
}
await this.createPolicy(newPolicy, this.getPolicyId(config, location));
}
} catch (e) {
this.server.logger.error(e);
return null;
if (!writeIntegrationPolicies) {
throw new Error(error);
}
}
async editMonitor(config: HeartbeatConfig) {
async createMonitor(config: HeartbeatConfig, request: KibanaRequest) {
const { locations } = config;
await this.checkPermissions(
request,
`Unable to create Synthetics package policy for monitor ${
config[ConfigKey.NAME]
}. Fleet write permissions are needed to use Synthetics private locations.`
);
const privateLocations: PrivateLocation[] = await getSyntheticsPrivateLocations(
this.server.authSavedObjectsClient!
);
const fleetManagedLocations = locations.filter((loc) => !loc.isServiceManaged);
for (const privateLocation of fleetManagedLocations) {
const location = privateLocations?.find((loc) => loc.id === privateLocation.id);
if (!location) {
throw new Error(
`Unable to find Synthetics private location for agentId ${privateLocation.id}`
);
}
const newPolicy = await this.generateNewPolicy(config, location);
if (!newPolicy) {
throw new Error(
`Unable to create Synthetics package policy for monitor ${
config[ConfigKey.NAME]
} with private location ${location.label}`
);
}
try {
await this.createPolicy(newPolicy, this.getPolicyId(config, location));
} catch (e) {
this.server.logger.error(e);
throw new Error(
`Unable to create Synthetics package policy for monitor ${
config[ConfigKey.NAME]
} with private location ${location.label}`
);
}
}
}
async editMonitor(config: HeartbeatConfig, request: KibanaRequest) {
await this.checkPermissions(
request,
`Unable to update Synthetics package policy for monitor ${
config[ConfigKey.NAME]
}. Fleet write permissions are needed to use Synthetics private locations.`
);
const { locations } = config;
const allPrivateLocations = await getSyntheticsPrivateLocations(
@ -131,7 +172,11 @@ export class SyntheticsPrivateLocation {
const newPolicy = await this.generateNewPolicy(config, privateLocation);
if (!newPolicy) {
throw new Error('Unable to create Synthetics package policy for private location');
throw new Error(
`Unable to ${
hasPolicy ? 'update' : 'create'
} Synthetics package policy for private location ${privateLocation.label}`
);
}
if (hasPolicy) {
@ -142,13 +187,26 @@ export class SyntheticsPrivateLocation {
} else if (hasPolicy) {
const soClient = this.server.authSavedObjectsClient!;
const esClient = this.server.uptimeEsClient.baseESClient;
await this.server.fleet.packagePolicyService.delete(soClient, esClient, [currId], {
force: true,
});
try {
await this.server.fleet.packagePolicyService.delete(soClient, esClient, [currId], {
force: true,
});
} catch (e) {
this.server.logger.error(e);
throw new Error(
`Unable to delete Synthetics package policy for monitor ${
config[ConfigKey.NAME]
} with private location ${privateLocation.label}`
);
}
}
} catch (e) {
this.server.logger.error(e);
return null;
throw new Error(
`Unable to ${hasPolicy ? 'update' : 'create'} Synthetics package policy for monitor ${
config[ConfigKey.NAME]
} with private location ${privateLocation.label}`
);
}
}
}
@ -157,15 +215,10 @@ export class SyntheticsPrivateLocation {
const soClient = this.server.authSavedObjectsClient;
const esClient = this.server.uptimeEsClient.baseESClient;
if (soClient && esClient) {
try {
return await this.server.fleet.packagePolicyService.create(soClient, esClient, newPolicy, {
id,
overwrite: true,
});
} catch (e) {
this.server.logger.error(e);
return null;
}
return await this.server.fleet.packagePolicyService.create(soClient, esClient, newPolicy, {
id,
overwrite: true,
});
}
}
@ -173,20 +226,15 @@ export class SyntheticsPrivateLocation {
const soClient = this.server.authSavedObjectsClient;
const esClient = this.server.uptimeEsClient.baseESClient;
if (soClient && esClient) {
try {
return await this.server.fleet.packagePolicyService.update(
soClient,
esClient,
id,
updatedPolicy,
{
force: true,
}
);
} catch (e) {
this.server.logger.error(e);
return null;
}
return await this.server.fleet.packagePolicyService.update(
soClient,
esClient,
id,
updatedPolicy,
{
force: true,
}
);
}
}
@ -200,33 +248,10 @@ export class SyntheticsPrivateLocation {
}
}
async findMonitor(config: HeartbeatConfig) {
const soClient = this.server.authSavedObjectsClient;
try {
const list = await this.server.fleet.packagePolicyService.list(soClient!, {
page: 1,
perPage: 10000,
kuery: `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name:synthetics`,
});
const { locations } = config;
const fleetManagedLocationIds = locations
.filter((loc) => !loc.isServiceManaged)
.map((loc) => config.id + '-' + loc.id);
return list.items.filter((policy) => {
return fleetManagedLocationIds.includes(policy.name);
});
} catch (e) {
this.server.logger.error(e);
return null;
}
}
async deleteMonitor(config: HeartbeatConfig) {
async deleteMonitor(config: HeartbeatConfig, request: KibanaRequest) {
const soClient = this.server.authSavedObjectsClient;
const esClient = this.server.uptimeEsClient.baseESClient;
if (soClient && esClient) {
const { locations } = config;
@ -237,6 +262,13 @@ export class SyntheticsPrivateLocation {
for (const privateLocation of monitorPrivateLocations) {
const location = allPrivateLocations?.find((loc) => loc.id === privateLocation.id);
if (location) {
await this.checkPermissions(
request,
`Unable to delete Synthetics package policy for monitor ${
config[ConfigKey.NAME]
}. Fleet write permissions are needed to use Synthetics private locations.`
);
try {
await this.server.fleet.packagePolicyService.delete(
soClient,
@ -248,7 +280,11 @@ export class SyntheticsPrivateLocation {
);
} catch (e) {
this.server.logger.error(e);
return null;
throw new Error(
`Unable to delete Synthetics package policy for monitor ${
config[ConfigKey.NAME]
} with private location ${location.label}`
);
}
}
}

View file

@ -171,6 +171,8 @@ export class ProjectMonitorFormatter {
monitor: normalizedMonitor,
monitorSavedObject: newMonitor,
syntheticsMonitorClient: this.syntheticsMonitorClient,
savedObjectsClient: this.savedObjectsClient,
request: this.request,
});
this.createdMonitors.push(monitor.id);
}
@ -182,6 +184,9 @@ export class ProjectMonitorFormatter {
details: e.message,
payload: monitor,
});
if (this.staleMonitorsMap[monitor.id]) {
this.staleMonitorsMap[monitor.id].stale = false;
}
}
};
@ -271,8 +276,11 @@ export class ProjectMonitorFormatter {
editedMonitor: normalizedMonitor,
editedMonitorSavedObject: editedMonitor,
previousMonitor,
decryptedPreviousMonitor,
server: this.server,
syntheticsMonitorClient: this.syntheticsMonitorClient,
savedObjectsClient: this.savedObjectsClient,
request: this.request,
});
}
@ -314,6 +322,7 @@ export class ProjectMonitorFormatter {
server: this.server,
monitorId,
syntheticsMonitorClient: this.syntheticsMonitorClient,
request: this.request,
});
this.deletedMonitors.push(journeyId);
} catch (e) {

View file

@ -4,11 +4,11 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { loggerMock } from '@kbn/logging-mocks';
import { KibanaRequest } from '@kbn/core/server';
import { SyntheticsMonitorClient } from './synthetics_monitor_client';
import { UptimeServerSetup } from '../../legacy_uptime/lib/adapters';
import { SyntheticsService } from '../synthetics_service';
import { loggerMock } from '@kbn/logging-mocks';
import times from 'lodash/times';
import {
LocationStatus,
@ -20,6 +20,7 @@ describe('SyntheticsMonitorClient', () => {
const mockEsClient = {
search: jest.fn(),
};
const mockRequest = {} as unknown as KibanaRequest;
const logger = loggerMock.create();
@ -84,7 +85,7 @@ describe('SyntheticsMonitorClient', () => {
const client = new SyntheticsMonitorClient(syntheticsService, serverMock);
client.privateLocationAPI.createMonitor = jest.fn();
await client.addMonitor(monitor, id);
await client.addMonitor(monitor, id, mockRequest);
expect(syntheticsService.addConfig).toHaveBeenCalledTimes(1);
expect(client.privateLocationAPI.createMonitor).toHaveBeenCalledTimes(1);
@ -97,7 +98,7 @@ describe('SyntheticsMonitorClient', () => {
const client = new SyntheticsMonitorClient(syntheticsService, serverMock);
client.privateLocationAPI.editMonitor = jest.fn();
await client.editMonitor(monitor, id);
await client.editMonitor(monitor, id, mockRequest);
expect(syntheticsService.editConfig).toHaveBeenCalledTimes(1);
expect(client.privateLocationAPI.editMonitor).toHaveBeenCalledTimes(1);
@ -109,7 +110,7 @@ describe('SyntheticsMonitorClient', () => {
const client = new SyntheticsMonitorClient(syntheticsService, serverMock);
client.privateLocationAPI.deleteMonitor = jest.fn();
await client.deleteMonitor(monitor as unknown as SyntheticsMonitorWithId);
await client.deleteMonitor(monitor as unknown as SyntheticsMonitorWithId, mockRequest);
expect(syntheticsService.deleteConfigs).toHaveBeenCalledTimes(1);
expect(client.privateLocationAPI.deleteMonitor).toHaveBeenCalledTimes(1);

View file

@ -4,7 +4,7 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { KibanaRequest } from '@kbn/core/server';
import { UptimeServerSetup } from '../../legacy_uptime/lib/adapters';
import { SyntheticsPrivateLocation } from '../private_location/synthetics_private_location';
import { SyntheticsService } from '../synthetics_service';
@ -26,7 +26,7 @@ export class SyntheticsMonitorClient {
this.privateLocationAPI = new SyntheticsPrivateLocation(server);
}
async addMonitor(monitor: MonitorFields, id: string) {
async addMonitor(monitor: MonitorFields, id: string, request: KibanaRequest) {
await this.syntheticsService.setupIndexTemplates();
const config = formatHeartbeatRequest({
@ -38,14 +38,15 @@ export class SyntheticsMonitorClient {
const { privateLocations, publicLocations } = this.parseLocations(config);
if (privateLocations.length > 0) {
await this.privateLocationAPI.createMonitor(config);
await this.privateLocationAPI.createMonitor(config, request);
}
if (publicLocations.length > 0) {
return await this.syntheticsService.addConfig(config);
}
}
async editMonitor(editedMonitor: MonitorFields, id: string) {
async editMonitor(editedMonitor: MonitorFields, id: string, request: KibanaRequest) {
const editedConfig = formatHeartbeatRequest({
monitor: editedMonitor,
monitorId: id,
@ -54,7 +55,7 @@ export class SyntheticsMonitorClient {
const { publicLocations } = this.parseLocations(editedConfig);
await this.privateLocationAPI.editMonitor(editedConfig);
await this.privateLocationAPI.editMonitor(editedConfig, request);
if (publicLocations.length > 0) {
return await this.syntheticsService.editConfig(editedConfig);
@ -62,8 +63,9 @@ export class SyntheticsMonitorClient {
await this.syntheticsService.editConfig(editedConfig);
}
async deleteMonitor(monitor: SyntheticsMonitorWithId) {
await this.privateLocationAPI.deleteMonitor(monitor);
async deleteMonitor(monitor: SyntheticsMonitorWithId, request: KibanaRequest) {
await this.privateLocationAPI.deleteMonitor(monitor, request);
return await this.syntheticsService.deleteConfigs([monitor]);
}

View file

@ -4,6 +4,7 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import uuid from 'uuid';
import { omit } from 'lodash';
import expect from '@kbn/expect';
import { secretKeys } from '@kbn/synthetics-plugin/common/constants/monitor_management';
@ -11,6 +12,7 @@ import { DataStream, HTTPFields } from '@kbn/synthetics-plugin/common/runtime_ty
import { API_URLS } from '@kbn/synthetics-plugin/common/constants';
import { DEFAULT_FIELDS } from '@kbn/synthetics-plugin/common/constants/monitor_defaults';
import { ALL_SPACES_ID } from '@kbn/security-plugin/common/constants';
import { syntheticsMonitorType } from '@kbn/synthetics-plugin/server/legacy_uptime/lib/saved_objects/synthetics_monitor';
import { format as formatUrl } from 'url';
import supertest from 'supertest';
import { serviceApiKeyPrivileges } from '@kbn/synthetics-plugin/server/synthetics_service/get_api_key';
@ -22,6 +24,9 @@ export default function ({ getService }: FtrProviderContext) {
this.tags('skipCloud');
const supertestAPI = getService('supertest');
const supertestWithoutAuth = getService('supertestWithoutAuth');
const security = getService('security');
const kibanaServer = getService('kibanaServer');
let _httpMonitorJson: HTTPFields;
let httpMonitorJson: HTTPFields;
@ -107,7 +112,7 @@ export default function ({ getService }: FtrProviderContext) {
);
});
it('cannot create a valid monitor without a monitor type', async () => {
it('cannot create a invalid monitor without a monitor type', async () => {
// Delete a required property to make payload invalid
const newMonitor = {
name: 'Sample name',
@ -222,5 +227,67 @@ export default function ({ getService }: FtrProviderContext) {
expect(apiResponse.body.message).eql('Unable to create synthetics-monitor');
});
});
it('handles private location errors and immediately deletes monitor if integration policy is unable to be saved', async () => {
const name = `Monitor with private location ${uuid.v4()}`;
const newMonitor = {
name,
type: 'http',
urls: 'https://elastic.co',
locations: [
{
id: 'policy-id',
label: 'Private Europe West',
isServiceManaged: false,
},
],
};
const username = 'admin';
const roleName = `synthetics_admin`;
const password = `${username}-password`;
const SPACE_ID = `test-space-${uuid.v4()}`;
const SPACE_NAME = `test-space-name ${uuid.v4()}`;
try {
await kibanaServer.spaces.create({ id: SPACE_ID, name: SPACE_NAME });
// use a user without fleet permissions to cause an error
await security.role.create(roleName, {
kibana: [
{
feature: {
uptime: ['all'],
},
spaces: ['*'],
},
],
});
await security.user.create(username, {
password,
roles: [roleName],
full_name: 'a kibana user',
});
await supertestWithoutAuth
.post(API_URLS.SYNTHETICS_MONITORS)
.auth(username, password)
.set('kbn-xsrf', 'true')
.send(newMonitor)
.expect(500);
const response = await supertestAPI
.get(API_URLS.SYNTHETICS_MONITORS)
.auth(username, password)
.query({
filter: `${syntheticsMonitorType}.attributes.name: "${name}"`,
})
.set('kbn-xsrf', 'true')
.expect(200);
expect(response.body.total).eql(0);
} finally {
await security.user.delete(username);
await security.role.delete(roleName);
}
});
});
}

View file

@ -13,7 +13,6 @@ import { PackagePolicy } from '@kbn/fleet-plugin/common';
import { FtrProviderContext } from '../../../ftr_provider_context';
import { getFixtureJson } from './helper/get_fixture_json';
import { PrivateLocationTestService } from './services/private_location_test_service';
import { comparePolicies, getTestProjectSyntheticsPolicy } from './sample_data/test_policy';
export default function ({ getService }: FtrProviderContext) {

View file

@ -4,33 +4,52 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import uuid from 'uuid';
import expect from '@kbn/expect';
import { HTTPFields, MonitorFields } from '@kbn/synthetics-plugin/common/runtime_types';
import { API_URLS } from '@kbn/synthetics-plugin/common/constants';
import { FtrProviderContext } from '../../../ftr_provider_context';
import { getFixtureJson } from './helper/get_fixture_json';
import { PrivateLocationTestService } from './services/private_location_test_service';
export default function ({ getService }: FtrProviderContext) {
describe('[DELETE] /internal/uptime/service/monitors', function () {
this.tags('skipCloud');
const supertest = getService('supertest');
const supertestWithoutAuth = getService('supertestWithoutAuth');
const security = getService('security');
const kibanaServer = getService('kibanaServer');
const testPrivateLocations = new PrivateLocationTestService(getService);
let _httpMonitorJson: HTTPFields;
let httpMonitorJson: HTTPFields;
let testPolicyId = '';
const saveMonitor = async (monitor: MonitorFields) => {
const res = await supertest
.post(API_URLS.SYNTHETICS_MONITORS)
.set('kbn-xsrf', 'true')
.send(monitor);
.send(monitor)
.expect(200);
return res.body;
};
before(() => {
before(async () => {
_httpMonitorJson = getFixtureJson('http_monitor');
await supertest.post('/api/fleet/setup').set('kbn-xsrf', 'true').send().expect(200);
await supertest
.post('/api/fleet/epm/packages/synthetics/0.9.5')
.set('kbn-xsrf', 'true')
.send({ force: true })
.expect(200);
const testPolicyName = 'Fleet test server policy' + Date.now();
const apiResponse = await testPrivateLocations.addFleetPolicy(testPolicyName);
testPolicyId = apiResponse.body.item.id;
await testPrivateLocations.setTestLocations([testPolicyId]);
});
beforeEach(() => {
@ -80,5 +99,71 @@ export default function ({ getService }: FtrProviderContext) {
.set('kbn-xsrf', 'true')
.expect(400);
});
it('handles private location errors and does not delete the monitor if integration policy is unable to be deleted', async () => {
const name = `Monitor with a private location ${uuid.v4()}`;
const newMonitor = {
name,
type: 'http',
urls: 'https://elastic.co',
locations: [
{
id: testPolicyId,
label: 'Private Europe West',
isServiceManaged: false,
},
],
};
const username = 'admin';
const roleName = `synthetics_admin`;
const password = `${username}-password`;
const SPACE_ID = `test-space-${uuid.v4()}`;
const SPACE_NAME = `test-space-name ${uuid.v4()}`;
let monitorId = '';
try {
await kibanaServer.spaces.create({ id: SPACE_ID, name: SPACE_NAME });
// use a user without fleet permissions to cause an error
await security.role.create(roleName, {
kibana: [
{
feature: {
uptime: ['all'],
},
spaces: ['*'],
},
],
});
await security.user.create(username, {
password,
roles: [roleName],
full_name: 'a kibana user',
});
const { id } = await saveMonitor(newMonitor as MonitorFields);
monitorId = id;
await supertestWithoutAuth
.delete(API_URLS.SYNTHETICS_MONITORS + '/' + monitorId)
.auth(username, password)
.set('kbn-xsrf', 'true')
.expect(500);
const response = await supertest
.get(`${API_URLS.SYNTHETICS_MONITORS}/${monitorId}`)
.set('kbn-xsrf', 'true')
.expect(200);
// ensure monitor was not deleted
expect(response.body.attributes.urls).eql(newMonitor.urls);
} finally {
await security.user.delete(username);
await security.role.delete(roleName);
await supertest
.delete(API_URLS.SYNTHETICS_MONITORS + '/' + monitorId)
.set('kbn-xsrf', 'true')
.expect(200);
}
});
});
}

View file

@ -4,6 +4,7 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import uuid from 'uuid';
import expect from '@kbn/expect';
import { omit } from 'lodash';
import { SimpleSavedObject } from '@kbn/core/public';
@ -12,14 +13,22 @@ import { ConfigKey, HTTPFields, MonitorFields } from '@kbn/synthetics-plugin/com
import { API_URLS } from '@kbn/synthetics-plugin/common/constants';
import { FtrProviderContext } from '../../../ftr_provider_context';
import { getFixtureJson } from './helper/get_fixture_json';
import { PrivateLocationTestService } from './services/private_location_test_service';
export default function ({ getService }: FtrProviderContext) {
describe('[PUT] /internal/uptime/service/monitors', function () {
this.tags('skipCloud');
const supertest = getService('supertest');
const supertestWithoutAuth = getService('supertestWithoutAuth');
const security = getService('security');
const kibanaServer = getService('kibanaServer');
const testPrivateLocations = new PrivateLocationTestService(getService);
let _httpMonitorJson: HTTPFields;
let httpMonitorJson: HTTPFields;
let testPolicyId = '';
const saveMonitor = async (monitor: MonitorFields) => {
const res = await supertest
@ -31,8 +40,14 @@ export default function ({ getService }: FtrProviderContext) {
return res.body as SimpleSavedObject<MonitorFields>;
};
before(() => {
before(async () => {
_httpMonitorJson = getFixtureJson('http_monitor');
await supertest.post('/api/fleet/setup').set('kbn-xsrf', 'true').send().expect(200);
const testPolicyName = 'Fleet test server policy' + Date.now();
const apiResponse = await testPrivateLocations.addFleetPolicy(testPolicyName);
testPolicyId = apiResponse.body.item.id;
await testPrivateLocations.setTestLocations([testPolicyId]);
});
beforeEach(() => {
@ -110,5 +125,81 @@ export default function ({ getService }: FtrProviderContext) {
expect(apiResponse.status).eql(400);
expect(apiResponse.body.message).eql('Monitor type is invalid');
});
it('handles private location errors and does not update the monitor if integration policy is unable to be updated', async () => {
const name = 'Monitor with private location';
const newMonitor = {
name,
type: 'http',
urls: 'https://elastic.co',
locations: [
{
id: 'us_central_west',
label: 'Europe West',
isServiceManaged: true,
},
],
};
const username = 'admin';
const roleName = `synthetics_admin`;
const password = `${username}-password`;
const SPACE_ID = `test-space-${uuid.v4()}`;
const SPACE_NAME = `test-space-name ${uuid.v4()}`;
let monitorId = '';
try {
await kibanaServer.spaces.create({ id: SPACE_ID, name: SPACE_NAME });
// use a user without fleet permissions to cause an error
await security.role.create(roleName, {
kibana: [
{
feature: {
uptime: ['all'],
},
spaces: ['*'],
},
],
});
await security.user.create(username, {
password,
roles: [roleName],
full_name: 'a kibana user',
});
const { id, attributes: savedMonitor } = await saveMonitor(newMonitor as MonitorFields);
monitorId = id;
const toUpdate = {
...savedMonitor,
locations: [
...savedMonitor.locations,
{ id: testPolicyId, label: 'Private location', isServiceManaged: false },
],
urls: 'https://google.com',
};
await supertestWithoutAuth
.put(API_URLS.SYNTHETICS_MONITORS + '/' + monitorId)
.auth(username, password)
.set('kbn-xsrf', 'true')
.send(toUpdate)
.expect(500);
const response = await supertest
.get(`${API_URLS.SYNTHETICS_MONITORS}/${monitorId}`)
.set('kbn-xsrf', 'true')
.expect(200);
// ensure monitor was not updated
expect(response.body.attributes.urls).eql(newMonitor.urls);
expect(response.body.attributes.locations).eql(newMonitor.locations);
} finally {
await security.user.delete(username);
await security.role.delete(roleName);
await supertest
.delete(API_URLS.SYNTHETICS_MONITORS + '/' + monitorId)
.set('kbn-xsrf', 'true')
.expect(200);
}
});
});
}