[8.8] [Synthetics] Private location better error handling (#152695) (#156807)

# Backport

This will backport the following commits from `main` to `8.8`:
- [[Synthetics] Private location better error handling
(#152695)](https://github.com/elastic/kibana/pull/152695)

<!--- Backport version: 8.9.7 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sqren/backport)

<!--BACKPORT
[{"author":{"name":"Shahzad","email":"shahzad31comp@gmail.com"},"sourceCommit":{"committedDate":"2023-05-05T09:31:59Z","message":"[Synthetics]
Private location better error handling (#152695)\n\nCo-authored-by:
Dominique Clarke
<dominique.clarke@elastic.co>","sha":"1aba7df3363d25a24b35fd0f716d5f89c76c41b8","branchLabelMapping":{"^v8.9.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["Team:uptime","release_note:skip","Team:Fleet","v8.8.0","v8.9.0"],"number":152695,"url":"https://github.com/elastic/kibana/pull/152695","mergeCommit":{"message":"[Synthetics]
Private location better error handling (#152695)\n\nCo-authored-by:
Dominique Clarke
<dominique.clarke@elastic.co>","sha":"1aba7df3363d25a24b35fd0f716d5f89c76c41b8"}},"sourceBranch":"main","suggestedTargetBranches":["8.8"],"targetPullRequestStates":[{"branch":"8.8","label":"v8.8.0","labelRegex":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"main","label":"v8.9.0","labelRegex":"^v8.9.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/152695","number":152695,"mergeCommit":{"message":"[Synthetics]
Private location better error handling (#152695)\n\nCo-authored-by:
Dominique Clarke
<dominique.clarke@elastic.co>","sha":"1aba7df3363d25a24b35fd0f716d5f89c76c41b8"}}]}]
BACKPORT-->

Co-authored-by: Shahzad <shahzad31comp@gmail.com>
Co-authored-by: Dominique Clarke <dominique.clarke@elastic.co>
This commit is contained in:
Kibana Machine 2023-05-08 16:43:43 -04:00 committed by GitHub
parent 3c53e3e42c
commit 387949f120
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
31 changed files with 1103 additions and 526 deletions

View file

@ -45,7 +45,11 @@ import type {
} from '../../common/types';
import { packageToPackagePolicy } from '../../common/services';
import { FleetError, PackagePolicyIneligibleForUpgradeError } from '../errors';
import {
FleetError,
PackagePolicyIneligibleForUpgradeError,
PackagePolicyValidationError,
} from '../errors';
import { PACKAGE_POLICY_SAVED_OBJECT_TYPE } from '../constants';
@ -1725,32 +1729,47 @@ describe('Package policy service', () => {
],
});
savedObjectsClient.update.mockImplementation(
savedObjectsClient.bulkUpdate.mockImplementation(
async (
type: string,
id: string,
attrs: any
): Promise<SavedObjectsUpdateResponse<PackagePolicySOAttributes>> => {
savedObjectsClient.get.mockResolvedValue({
objs: Array<{
type: string;
id: string;
attributes: any;
}>
) => {
const newObjs = objs.map((obj) => ({
id: 'test',
type: 'abcd',
references: [],
version: 'test',
attributes: attrs,
attributes: obj.attributes,
}));
savedObjectsClient.bulkGet.mockResolvedValue({
saved_objects: newObjs,
});
return attrs;
return {
saved_objects: newObjs,
};
}
);
const elasticsearchClient = elasticsearchServiceMock.createClusterClient().asInternalUser;
const res = packagePolicyService.bulkUpdate(
const toUpdate = { ...mockPackagePolicy, inputs: inputsUpdate };
const res = await packagePolicyService.bulkUpdate(
savedObjectsClient,
elasticsearchClient,
[{ ...mockPackagePolicy, inputs: inputsUpdate }]
[toUpdate]
);
await expect(res).rejects.toThrow('cat is a frozen variable and cannot be modified');
expect(res.failedPolicies).toHaveLength(1);
expect(res.updatedPolicies).toHaveLength(0);
expect(res.failedPolicies[0].packagePolicy).toEqual(toUpdate);
expect(res.failedPolicies[0].error).toEqual(
new PackagePolicyValidationError(`cat is a frozen variable and cannot be modified`)
);
});
it('should allow to update input vars that are frozen with the force flag', async () => {
@ -1882,7 +1901,11 @@ describe('Package policy service', () => {
{ force: true }
);
const [modifiedInput] = result![0].inputs;
expect(result.updatedPolicies).toHaveLength(1);
const updatedPolicy = result.updatedPolicies?.[0]!;
const [modifiedInput] = updatedPolicy.inputs;
expect(modifiedInput.enabled).toEqual(true);
expect(modifiedInput.vars!.dog.value).toEqual('labrador');
expect(modifiedInput.vars!.cat.value).toEqual('tabby');
@ -2016,7 +2039,11 @@ describe('Package policy service', () => {
[{ ...mockPackagePolicy, inputs: inputsUpdate }]
);
const [modifiedInput] = result![0].inputs;
expect(result.updatedPolicies).toHaveLength(1);
const updatedPolicy = result.updatedPolicies?.[0]!;
const [modifiedInput] = updatedPolicy.inputs;
expect(modifiedInput.enabled).toEqual(true);
expect(modifiedInput.vars!.dog.value).toEqual('labrador');
expect(modifiedInput.vars!.cat.value).toEqual('siamese');
@ -2082,13 +2109,15 @@ describe('Package policy service', () => {
const elasticsearchClient = elasticsearchServiceMock.createClusterClient().asInternalUser;
const result = await packagePolicyService.bulkUpdate(
const { updatedPolicies } = await packagePolicyService.bulkUpdate(
savedObjectsClient,
elasticsearchClient,
[{ ...mockPackagePolicy, inputs: [] }]
);
expect(result![0].elasticsearch).toMatchObject({ privileges: { cluster: ['monitor'] } });
expect(updatedPolicies![0].elasticsearch).toMatchObject({
privileges: { cluster: ['monitor'] },
});
});
it('should not mutate packagePolicyUpdate object when trimming whitespace', async () => {
@ -2138,7 +2167,7 @@ describe('Package policy service', () => {
const elasticsearchClient = elasticsearchServiceMock.createClusterClient().asInternalUser;
const result = await packagePolicyService.bulkUpdate(
const { updatedPolicies } = await packagePolicyService.bulkUpdate(
savedObjectsClient,
elasticsearchClient,
// this mimics the way that OSQuery plugin create immutable objects
@ -2150,7 +2179,7 @@ describe('Package policy service', () => {
]
);
expect(result![0].name).toEqual('test');
expect(updatedPolicies![0].name).toEqual('test');
});
it('should send telemetry event when upgrading a package policy', async () => {

View file

@ -16,6 +16,8 @@ import type {
SavedObjectsClientContract,
Logger,
RequestHandlerContext,
SavedObjectsBulkCreateObject,
SavedObjectsBulkUpdateObject,
} from '@kbn/core/server';
import { SavedObjectsUtils } from '@kbn/core/server';
import { v4 as uuidv4 } from 'uuid';
@ -27,6 +29,8 @@ import { type AuthenticatedUser } from '@kbn/security-plugin/server';
import pMap from 'p-map';
import type { SavedObjectError } from '@kbn/core-saved-objects-common';
import { HTTPAuthorizationHeader } from '../../common/http_authorization_header';
import {
@ -301,7 +305,10 @@ class PackagePolicyClientImpl implements PackagePolicyClient {
bumpRevision?: boolean;
force?: true;
}
): Promise<PackagePolicy[]> {
): Promise<{
created: PackagePolicy[];
failed: Array<{ packagePolicy: NewPackagePolicy; error?: Error | SavedObjectError }>;
}> {
for (const packagePolicy of packagePolicies) {
if (!packagePolicy.id) {
packagePolicy.id = SavedObjectsUtils.generateId();
@ -323,9 +330,24 @@ class PackagePolicyClientImpl implements PackagePolicyClient {
const packageInfos = await getPackageInfoForPackagePolicies(packagePolicies, soClient);
const isoDate = new Date().toISOString();
// eslint-disable-next-line @typescript-eslint/naming-convention
const { saved_objects } = await soClient.bulkCreate<PackagePolicySOAttributes>(
await pMap(packagePolicies, async (packagePolicy) => {
const policiesToCreate: Array<SavedObjectsBulkCreateObject<PackagePolicySOAttributes>> = [];
const failedPolicies: Array<{
packagePolicy: NewPackagePolicyWithId;
error: Error | SavedObjectError;
}> = [];
const logger = appContextService.getLogger();
const packagePoliciesWithIds = packagePolicies.map((p) => {
if (!p.id) {
p.id = SavedObjectsUtils.generateId();
}
return p;
});
await pMap(packagePoliciesWithIds, async (packagePolicy) => {
try {
const packagePolicyId = packagePolicy.id ?? uuidv4();
const agentPolicyId = packagePolicy.policy_id;
@ -348,7 +370,7 @@ class PackagePolicyClientImpl implements PackagePolicyClient {
elasticsearch = pkgInfo?.elasticsearch;
}
return {
policiesToCreate.push({
type: SAVED_OBJECT_TYPE,
id: packagePolicyId,
attributes: {
@ -365,12 +387,32 @@ class PackagePolicyClientImpl implements PackagePolicyClient {
updated_at: isoDate,
updated_by: options?.user?.username ?? 'system',
},
};
})
});
} catch (error) {
failedPolicies.push({ packagePolicy, error });
logger.error(error);
}
});
const { saved_objects: createdObjects } = await soClient.bulkCreate<PackagePolicySOAttributes>(
policiesToCreate
);
// Filter out invalid SOs
const newSos = saved_objects.filter((so) => !so.error && so.attributes);
const newSos = createdObjects.filter((so) => !so.error && so.attributes);
packagePoliciesWithIds.forEach((packagePolicy) => {
const hasCreatedSO = newSos.find((so) => so.id === packagePolicy.id);
const hasFailed = failedPolicies.some(
({ packagePolicy: failedPackagePolicy }) => failedPackagePolicy.id === packagePolicy.id
);
if (hasCreatedSO?.error && !hasFailed) {
failedPolicies.push({
packagePolicy,
error: hasCreatedSO?.error ?? new Error('Failed to create package policy.'),
});
}
});
// Assign it to the given agent policy
@ -382,11 +424,14 @@ class PackagePolicyClientImpl implements PackagePolicyClient {
}
}
return newSos.map((newSo) => ({
id: newSo.id,
version: newSo.version,
...newSo.attributes,
}));
return {
created: newSos.map((newSo) => ({
id: newSo.id,
version: newSo.version,
...newSo.attributes,
})),
failed: failedPolicies,
};
}
public async get(
@ -730,7 +775,13 @@ class PackagePolicyClientImpl implements PackagePolicyClient {
packagePolicyUpdates: Array<NewPackagePolicy & { version?: string; id: string }>,
options?: { user?: AuthenticatedUser; force?: boolean },
currentVersion?: string
): Promise<PackagePolicy[] | null> {
): Promise<{
updatedPolicies: PackagePolicy[] | null;
failedPolicies: Array<{
packagePolicy: NewPackagePolicyWithId;
error: Error | SavedObjectError;
}>;
}> {
for (const packagePolicy of packagePolicyUpdates) {
auditLoggingService.writeCustomSoAuditLog({
action: 'update',
@ -749,8 +800,14 @@ class PackagePolicyClientImpl implements PackagePolicyClient {
const packageInfos = await getPackageInfoForPackagePolicies(packagePolicyUpdates, soClient);
const { saved_objects: newPolicies } = await soClient.bulkUpdate<PackagePolicySOAttributes>(
await pMap(packagePolicyUpdates, async (packagePolicyUpdate) => {
const policiesToUpdate: Array<SavedObjectsBulkUpdateObject<PackagePolicySOAttributes>> = [];
const failedPolicies: Array<{
packagePolicy: NewPackagePolicyWithId;
error: Error | SavedObjectError;
}> = [];
await pMap(packagePolicyUpdates, async (packagePolicyUpdate) => {
try {
const id = packagePolicyUpdate.id;
const packagePolicy = { ...packagePolicyUpdate, name: packagePolicyUpdate.name.trim() };
const oldPackagePolicy = oldPackagePolicies.find((p) => p.id === id);
@ -786,7 +843,7 @@ class PackagePolicyClientImpl implements PackagePolicyClient {
// Handle component template/mappings updates for experimental features, e.g. synthetic source
await handleExperimentalDatastreamFeatureOptIn({ soClient, esClient, packagePolicy });
return {
policiesToUpdate.push({
type: SAVED_OBJECT_TYPE,
id,
attributes: {
@ -803,8 +860,14 @@ class PackagePolicyClientImpl implements PackagePolicyClient {
updated_by: options?.user?.username ?? 'system',
},
version,
};
})
});
} catch (error) {
failedPolicies.push({ packagePolicy: packagePolicyUpdate, error });
}
});
const { saved_objects: updatedPolicies } = await soClient.bulkUpdate<PackagePolicySOAttributes>(
policiesToUpdate
);
const agentPolicyIds = new Set(packagePolicyUpdates.map((p) => p.policy_id));
@ -839,14 +902,32 @@ class PackagePolicyClientImpl implements PackagePolicyClient {
sendUpdatePackagePolicyTelemetryEvent(soClient, packagePolicyUpdates, oldPackagePolicies);
return newPolicies.map(
(soPolicy) =>
({
id: soPolicy.id,
version: soPolicy.version,
...soPolicy.attributes,
} as PackagePolicy)
);
updatedPolicies.forEach((policy) => {
if (policy.error) {
const hasAlreadyFailed = failedPolicies.some(
(failedPolicy) => failedPolicy.packagePolicy.id === policy.id
);
if (!hasAlreadyFailed) {
failedPolicies.push({
packagePolicy: packagePolicyUpdates.find((p) => p.id === policy.id)!,
error: policy.error,
});
}
}
});
const updatedPoliciesSuccess = updatedPolicies
.filter((policy) => !policy.error && policy.attributes)
.map(
(soPolicy) =>
({
id: soPolicy.id,
version: soPolicy.version,
...soPolicy.attributes,
} as PackagePolicy)
);
return { updatedPolicies: updatedPoliciesSuccess, failedPolicies };
}
public async delete(
@ -1920,6 +2001,7 @@ function _enforceFrozenVars(
export interface NewPackagePolicyWithId extends NewPackagePolicy {
id?: string;
policy_id: string;
version?: string;
}
export const packagePolicyService: PackagePolicyClient = new PackagePolicyClientImpl();

View file

@ -9,6 +9,8 @@ import type { KibanaRequest, Logger, RequestHandlerContext } from '@kbn/core/ser
import type { ElasticsearchClient, SavedObjectsClientContract } from '@kbn/core/server';
import type { AuthenticatedUser } from '@kbn/security-plugin/server';
import type { SavedObjectError } from '@kbn/core-saved-objects-common';
import type { HTTPAuthorizationHeader } from '../../common/http_authorization_header';
import type {
@ -64,15 +66,24 @@ export interface PackagePolicyClient {
force?: true;
authorizationHeader?: HTTPAuthorizationHeader | null;
}
): Promise<PackagePolicy[]>;
): Promise<{
created: PackagePolicy[];
failed: Array<{ packagePolicy: NewPackagePolicy; error?: Error | SavedObjectError }>;
}>;
bulkUpdate(
soClient: SavedObjectsClientContract,
esClient: ElasticsearchClient,
packagePolicyUpdates: Array<NewPackagePolicy & { version?: string; id: string }>,
packagePolicyUpdates: UpdatePackagePolicy[],
options?: { user?: AuthenticatedUser; force?: boolean },
currentVersion?: string
): Promise<PackagePolicy[] | null>;
): Promise<{
updatedPolicies: PackagePolicy[] | null;
failedPolicies: Array<{
packagePolicy: NewPackagePolicyWithId;
error: Error | SavedObjectError;
}>;
}>;
get(soClient: SavedObjectsClientContract, id: string): Promise<PackagePolicy | null>;

View file

@ -7,7 +7,6 @@
import type { SimpleSavedObject } from '@kbn/core/public';
import {
EncryptedSyntheticsMonitor,
Locations,
MonitorFields,
ServiceLocationErrors,
@ -19,19 +18,10 @@ export interface MonitorIdParam {
monitorId: string;
}
export type SyntheticsMonitorSavedObject = SimpleSavedObject<EncryptedSyntheticsMonitor> & {
updated_at: string;
};
export type DecryptedSyntheticsMonitorSavedObject = SimpleSavedObject<SyntheticsMonitor> & {
updated_at: string;
};
export interface SyntheticsServiceAllowed {
serviceAllowed: boolean;
signupUrl: string;
}
export interface TestNowResponse {
schedule: SyntheticsMonitorSchedule;
locations: Locations;

View file

@ -15,7 +15,7 @@ import { MONITOR_EDIT_ROUTE } from '../../../../../../common/constants';
import { SyntheticsMonitor } from '../../../../../../common/runtime_types';
import { createMonitorAPI, updateMonitorAPI } from '../../../state/monitor_management/api';
import { kibanaService } from '../../../../../utils/kibana_service';
import { cleanMonitorListState } from '../../../state';
import { cleanMonitorListState, IHttpSerializedFetchError } from '../../../state';
import { useSyntheticsRefreshContext } from '../../../contexts';
export const useMonitorSave = ({ monitorData }: { monitorData?: SyntheticsMonitor }) => {
@ -28,7 +28,7 @@ export const useMonitorSave = ({ monitorData }: { monitorData?: SyntheticsMonito
const editRouteMatch = useRouteMatch({ path: MONITOR_EDIT_ROUTE });
const isEdit = editRouteMatch?.isExact;
const { data, status, loading } = useFetcher(() => {
const { data, status, loading, error } = useFetcher(() => {
if (monitorData) {
if (isEdit) {
return updateMonitorAPI({
@ -44,11 +44,14 @@ export const useMonitorSave = ({ monitorData }: { monitorData?: SyntheticsMonito
}, [monitorData]);
useEffect(() => {
if (status === FETCH_STATUS.FAILURE) {
kibanaService.toasts.addDanger({
title: MONITOR_FAILURE_LABEL,
toastLifeTimeMs: 3000,
});
if (status === FETCH_STATUS.FAILURE && error) {
kibanaService.toasts.addError(
{
...error,
message: (error as unknown as IHttpSerializedFetchError).body.message ?? error.message,
},
{ title: MONITOR_FAILURE_LABEL }
);
} else if (status === FETCH_STATUS.SUCCESS && !loading) {
refreshApp();
dispatch(cleanMonitorListState());
@ -63,7 +66,7 @@ export const useMonitorSave = ({ monitorData }: { monitorData?: SyntheticsMonito
toastLifeTimeMs: 3000,
});
}
}, [data, status, monitorId, loading, refreshApp, dispatch, theme$]);
}, [data, status, monitorId, loading, refreshApp, dispatch, theme$, error]);
return { status, loading, isEdit };
};

View file

@ -120,6 +120,7 @@ export interface RouteContext {
server: UptimeServerSetup;
syntheticsMonitorClient: SyntheticsMonitorClient;
subject?: Subject<unknown>;
spaceId: string;
}
export type SyntheticsRouteHandler = ({
@ -139,6 +140,7 @@ export type SyntheticsStreamingRouteHandler = ({
server,
savedObjectsClient,
subject: Subject,
spaceId,
}: {
uptimeEsClient: UptimeEsClient;
context: UptimeRequestHandlerContext;
@ -147,4 +149,5 @@ export type SyntheticsStreamingRouteHandler = ({
server: UptimeServerSetup;
syntheticsMonitorClient: SyntheticsMonitorClient;
subject?: Subject<unknown>;
spaceId: string;
}) => IKibanaResponse<any> | Promise<IKibanaResponse<any>>;

View file

@ -15,7 +15,6 @@ import {
import { isValidNamespace } from '@kbn/fleet-plugin/common';
import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common';
import { getSyntheticsPrivateLocations } from '../../legacy_uptime/lib/saved_objects/private_locations';
import { SyntheticsMonitorClient } from '../../synthetics_service/synthetics_monitor/synthetics_monitor_client';
import {
ConfigKey,
MonitorFields,
@ -24,7 +23,7 @@ import {
PrivateLocation,
} from '../../../common/runtime_types';
import { formatKibanaNamespace } from '../../../common/formatters';
import { SyntheticsRestApiRouteFactory } from '../../legacy_uptime/routes/types';
import { RouteContext, SyntheticsRestApiRouteFactory } from '../../legacy_uptime/routes/types';
import { API_URLS } from '../../../common/constants';
import {
DEFAULT_FIELDS,
@ -48,14 +47,9 @@ export const addSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () => ({
preserve_namespace: schema.maybe(schema.boolean()),
}),
},
handler: async ({
context,
request,
response,
savedObjectsClient,
server,
syntheticsMonitorClient,
}): Promise<any> => {
writeAccess: true,
handler: async (routeContext): Promise<any> => {
const { context, request, response, savedObjectsClient, server } = routeContext;
// usually id is auto generated, but this is useful for testing
const { id } = request.query;
@ -73,18 +67,18 @@ export const addSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () => ({
return response.badRequest({ body: { message, attributes: { details, ...payload } } });
}
const privateLocations: PrivateLocation[] = await getSyntheticsPrivateLocations(
savedObjectsClient
const normalizedMonitor = validationResult.decodedMonitor;
const privateLocations: PrivateLocation[] = await getPrivateLocations(
savedObjectsClient,
normalizedMonitor
);
try {
const spaceId = server.spaces?.spacesService.getSpaceId(request) ?? DEFAULT_SPACE_ID;
const { errors, newMonitor } = await syncNewMonitor({
normalizedMonitor: validationResult.decodedMonitor,
server,
syntheticsMonitorClient,
savedObjectsClient,
request,
normalizedMonitor,
routeContext,
id,
privateLocations,
spaceId,
@ -122,7 +116,7 @@ export const addSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () => ({
}
return response.customError({
body: { message: 'Unable to create monitor' },
body: { message: getErr.message },
statusCode: 500,
});
}
@ -157,23 +151,18 @@ export const createNewSavedObjectMonitor = async ({
export const syncNewMonitor = async ({
id,
server,
syntheticsMonitorClient,
savedObjectsClient,
request,
normalizedMonitor,
privateLocations,
spaceId,
routeContext,
}: {
id?: string;
normalizedMonitor: SyntheticsMonitor;
server: UptimeServerSetup;
syntheticsMonitorClient: SyntheticsMonitorClient;
savedObjectsClient: SavedObjectsClientContract;
request: KibanaRequest;
routeContext: RouteContext;
privateLocations: PrivateLocation[];
spaceId: string;
}) => {
const { savedObjectsClient, server, syntheticsMonitorClient, request } = routeContext;
const newMonitorId = id ?? uuidV4();
const { preserve_namespace: preserveNamespace } = request.query as Record<
string,
@ -205,10 +194,18 @@ export const syncNewMonitor = async ({
spaceId
);
const [monitorSavedObjectN, { syncErrors }] = await Promise.all([
const [monitorSavedObjectN, [packagePolicyResult, syncErrors]] = await Promise.all([
newMonitorPromise,
syncErrorsPromise,
]);
]).catch((e) => {
server.logger.error(e);
throw e;
});
if (packagePolicyResult && (packagePolicyResult?.failed?.length ?? []) > 0) {
const failed = packagePolicyResult.failed.map((f) => f.error);
throw new Error(failed.join(', '));
}
monitorSavedObject = monitorSavedObjectN;
@ -225,21 +222,59 @@ export const syncNewMonitor = async ({
return { errors: syncErrors, newMonitor: monitorSavedObject };
} catch (e) {
if (monitorSavedObject?.id) {
await deleteMonitor({
savedObjectsClient,
server,
monitorId: newMonitorId,
syntheticsMonitorClient,
request,
});
}
server.logger.error(
`Unable to create Synthetics monitor ${monitorWithNamespace[ConfigKey.NAME]}`
);
await deleteMonitorIfCreated({
newMonitorId,
routeContext,
});
server.logger.error(e);
throw e;
}
};
export const deleteMonitorIfCreated = async ({
newMonitorId,
routeContext,
}: {
routeContext: RouteContext;
newMonitorId: string;
}) => {
const { server, savedObjectsClient } = routeContext;
try {
const encryptedMonitor = await savedObjectsClient.get<EncryptedSyntheticsMonitor>(
syntheticsMonitorType,
newMonitorId
);
if (encryptedMonitor) {
await savedObjectsClient.delete(syntheticsMonitorType, newMonitorId);
await deleteMonitor({
routeContext,
monitorId: newMonitorId,
});
}
} catch (e) {
// ignore errors here
server.logger.error(e);
}
};
const getPrivateLocations = async (
soClient: SavedObjectsClientContract,
normalizedMonitor: SyntheticsMonitor
) => {
const { locations } = normalizedMonitor;
const hasPrivateLocation = locations.filter((location) => !location.isServiceManaged);
if (hasPrivateLocation.length === 0) {
return [];
}
return await getSyntheticsPrivateLocations(soClient);
};
const getMonitorNamespace = (
server: UptimeServerSetup,
request: KibanaRequest,

View file

@ -31,14 +31,8 @@ export const addSyntheticsProjectMonitorRoute: SyntheticsRestApiRouteFactory = (
maxBytes: MAX_PAYLOAD_SIZE,
},
},
handler: async ({
context,
request,
response,
savedObjectsClient,
server,
syntheticsMonitorClient,
}): Promise<any> => {
handler: async (routeContext): Promise<any> => {
const { request, response, server } = routeContext;
const { projectName } = request.params;
const decodedProjectName = decodeURI(projectName);
const monitors = (request.body?.monitors as ProjectMonitor[]) || [];
@ -59,14 +53,11 @@ export const addSyntheticsProjectMonitorRoute: SyntheticsRestApiRouteFactory = (
const encryptedSavedObjectsClient = server.encryptedSavedObjects.getClient();
const pushMonitorFormatter = new ProjectMonitorFormatter({
routeContext,
projectId: decodedProjectName,
spaceId,
encryptedSavedObjectsClient,
savedObjectsClient,
monitors,
server,
syntheticsMonitorClient,
request,
});
await pushMonitorFormatter.configureAllProjectMonitors();

View file

@ -4,10 +4,14 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { SavedObjectsClientContract, KibanaRequest, SavedObject } from '@kbn/core/server';
import { SavedObjectsClientContract, 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';
import { NewPackagePolicy } from '@kbn/fleet-plugin/common';
import { SavedObjectError } from '@kbn/core-saved-objects-common';
import { RouteContext } from '../../../legacy_uptime/routes';
import { deleteMonitorIfCreated } from '../add_monitor';
import { formatTelemetryEvent, sendTelemetryEvents } from '../../telemetry/monitor_upgrade_sender';
import { deleteMonitor } from '../delete_monitor';
import { UptimeServerSetup } from '../../../legacy_uptime/lib/adapters';
@ -21,7 +25,6 @@ import {
ServiceLocationErrors,
SyntheticsMonitor,
} from '../../../../common/runtime_types';
import { SyntheticsMonitorClient } from '../../../synthetics_service/synthetics_monitor/synthetics_monitor_client';
export const createNewSavedObjectMonitorBulk = async ({
soClient,
@ -41,27 +44,28 @@ export const createNewSavedObjectMonitorBulk = async ({
}),
}));
return await soClient.bulkCreate<EncryptedSyntheticsMonitor>(newMonitors);
const result = await soClient.bulkCreate<EncryptedSyntheticsMonitor>(newMonitors);
return result.saved_objects;
};
type MonitorSavedObject = SavedObject<EncryptedSyntheticsMonitor>;
type CreatedMonitors = SavedObjectsBulkResponse<EncryptedSyntheticsMonitor>['saved_objects'];
export const syncNewMonitorBulk = async ({
routeContext,
normalizedMonitors,
server,
syntheticsMonitorClient,
soClient,
request,
privateLocations,
spaceId,
}: {
routeContext: RouteContext;
normalizedMonitors: SyntheticsMonitor[];
server: UptimeServerSetup;
syntheticsMonitorClient: SyntheticsMonitorClient;
soClient: SavedObjectsClientContract;
request: KibanaRequest;
privateLocations: PrivateLocation[];
spaceId: string;
}) => {
let newMonitors: SavedObjectsBulkResponse<EncryptedSyntheticsMonitor> | null = null;
const { server, savedObjectsClient, syntheticsMonitorClient, request } = routeContext;
let newMonitors: CreatedMonitors | null = null;
const monitorsToCreate = normalizedMonitors.map((monitor) => {
const monitorSavedObjectId = uuidV4();
return {
@ -76,57 +80,86 @@ export const syncNewMonitorBulk = async ({
});
try {
const [createdMonitors, { syncErrors }] = await Promise.all([
const [createdMonitors, [policiesResult, syncErrors]] = await Promise.all([
createNewSavedObjectMonitorBulk({
monitorsToCreate,
soClient,
soClient: savedObjectsClient,
}),
syntheticsMonitorClient.addMonitors(
monitorsToCreate,
request,
soClient,
savedObjectsClient,
privateLocations,
spaceId
),
]);
let failedMonitors: FailedMonitorConfig[] = [];
const { failed: failedPolicies } = policiesResult ?? {};
newMonitors = createdMonitors;
sendNewMonitorTelemetry(server, newMonitors.saved_objects, syncErrors);
if (failedPolicies && failedPolicies?.length > 0 && newMonitors) {
failedMonitors = await handlePrivateConfigErrors(routeContext, newMonitors, failedPolicies);
}
return { errors: syncErrors, newMonitors: newMonitors.saved_objects };
sendNewMonitorTelemetry(server, newMonitors, syncErrors);
return { errors: syncErrors, newMonitors, failedMonitors };
} catch (e) {
await rollBackNewMonitorBulk(
monitorsToCreate,
server,
soClient,
syntheticsMonitorClient,
request
);
await rollBackNewMonitorBulk(monitorsToCreate, routeContext);
throw e;
}
};
interface FailedMonitorConfig {
monitor: MonitorSavedObject;
error?: Error | SavedObjectError;
}
const handlePrivateConfigErrors = async (
routeContext: RouteContext,
createdMonitors: CreatedMonitors,
failedPolicies: Array<{ packagePolicy: NewPackagePolicy; error?: Error | SavedObjectError }>
) => {
const failedMonitors: FailedMonitorConfig[] = [];
await pMap(failedPolicies, async ({ packagePolicy, error }) => {
const { inputs } = packagePolicy;
const enabledInput = inputs?.find((input) => input.enabled);
const stream = enabledInput?.streams?.[0];
const vars = stream?.vars;
const monitorId = vars?.[ConfigKey.CONFIG_ID]?.value;
const monitor = createdMonitors.find(
(savedObject) => savedObject.attributes[ConfigKey.CONFIG_ID] === monitorId
);
if (monitor) {
failedMonitors.push({ monitor, error });
await deleteMonitorIfCreated({
routeContext,
newMonitorId: monitor.id,
});
createdMonitors.splice(createdMonitors.indexOf(monitor), 1);
}
});
return failedMonitors;
};
const rollBackNewMonitorBulk = async (
monitorsToCreate: Array<{ id: string; monitor: MonitorFields }>,
server: UptimeServerSetup,
soClient: SavedObjectsClientContract,
syntheticsMonitorClient: SyntheticsMonitorClient,
request: KibanaRequest
routeContext: RouteContext
) => {
const { server } = routeContext;
try {
await pMap(
monitorsToCreate,
async (monitor) =>
deleteMonitor({
server,
request,
savedObjectsClient: soClient,
routeContext,
monitorId: monitor.id,
syntheticsMonitorClient,
}),
{ concurrency: 100 }
{ concurrency: 100, stopOnError: false }
);
} catch (e) {
// ignore errors here

View file

@ -4,110 +4,107 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { SavedObject, SavedObjectsUpdateResponse } from '@kbn/core/server';
import { SavedObjectError } from '@kbn/core-saved-objects-common';
import { FailedPolicyUpdate } from '../../../synthetics_service/private_location/synthetics_private_location';
import { RouteContext } from '../../../legacy_uptime/routes';
import {
SavedObjectsUpdateResponse,
SavedObject,
SavedObjectsClientContract,
KibanaRequest,
} from '@kbn/core/server';
import { SyntheticsMonitorClient } from '../../../synthetics_service/synthetics_monitor/synthetics_monitor_client';
import {
MonitorFields,
EncryptedSyntheticsMonitor,
SyntheticsMonitorWithSecrets,
SyntheticsMonitor,
ConfigKey,
EncryptedSyntheticsMonitor,
HeartbeatConfig,
MonitorFields,
PrivateLocation,
SyntheticsMonitor,
SyntheticsMonitorWithSecrets,
} from '../../../../common/runtime_types';
import { syntheticsMonitorType } from '../../../legacy_uptime/lib/saved_objects/synthetics_monitor';
import {
sendTelemetryEvents,
formatTelemetryUpdateEvent,
sendTelemetryEvents,
} from '../../telemetry/monitor_upgrade_sender';
import type { UptimeServerSetup } from '../../../legacy_uptime/lib/adapters/framework';
// Simplify return promise type and type it with runtime_types
interface MonitorConfigUpdate {
normalizedMonitor: SyntheticsMonitor;
monitorWithRevision: SyntheticsMonitorWithSecrets;
previousMonitor: SavedObject<EncryptedSyntheticsMonitor>;
decryptedPreviousMonitor: SavedObject<SyntheticsMonitorWithSecrets>;
}
const updateConfigSavedObjects = async ({
routeContext,
monitorsToUpdate,
}: {
routeContext: RouteContext;
monitorsToUpdate: MonitorConfigUpdate[];
}) => {
return await routeContext.savedObjectsClient.bulkUpdate<MonitorFields>(
monitorsToUpdate.map(({ previousMonitor, monitorWithRevision }) => ({
type: syntheticsMonitorType,
id: previousMonitor.id,
attributes: {
...monitorWithRevision,
[ConfigKey.CONFIG_ID]: previousMonitor.id,
[ConfigKey.MONITOR_QUERY_ID]:
monitorWithRevision[ConfigKey.CUSTOM_HEARTBEAT_ID] || previousMonitor.id,
},
}))
);
};
async function syncUpdatedMonitors({
spaceId,
privateLocations,
routeContext,
monitorsToUpdate,
}: {
privateLocations: PrivateLocation[];
spaceId: string;
routeContext: RouteContext;
monitorsToUpdate: MonitorConfigUpdate[];
}) {
const { syntheticsMonitorClient } = routeContext;
return await syntheticsMonitorClient.editMonitors(
monitorsToUpdate.map(({ normalizedMonitor, previousMonitor, decryptedPreviousMonitor }) => ({
monitor: {
...(normalizedMonitor as MonitorFields),
[ConfigKey.CONFIG_ID]: previousMonitor.id,
[ConfigKey.MONITOR_QUERY_ID]:
normalizedMonitor[ConfigKey.CUSTOM_HEARTBEAT_ID] || previousMonitor.id,
},
id: previousMonitor.id,
previousMonitor,
decryptedPreviousMonitor,
})),
routeContext,
privateLocations,
spaceId
);
}
export const syncEditedMonitorBulk = async ({
server,
request,
routeContext,
spaceId,
monitorsToUpdate,
savedObjectsClient,
privateLocations,
syntheticsMonitorClient,
}: {
monitorsToUpdate: Array<{
normalizedMonitor: SyntheticsMonitor;
monitorWithRevision: SyntheticsMonitorWithSecrets;
previousMonitor: SavedObject<EncryptedSyntheticsMonitor>;
decryptedPreviousMonitor: SavedObject<SyntheticsMonitorWithSecrets>;
}>;
server: UptimeServerSetup;
syntheticsMonitorClient: SyntheticsMonitorClient;
savedObjectsClient: SavedObjectsClientContract;
request: KibanaRequest;
monitorsToUpdate: MonitorConfigUpdate[];
routeContext: RouteContext;
privateLocations: PrivateLocation[];
spaceId: string;
}) => {
let savedObjectsSuccessful = false;
let syncSuccessful = false;
const { server } = routeContext;
try {
async function updateSavedObjects() {
try {
const editedSOPromise = await savedObjectsClient.bulkUpdate<MonitorFields>(
monitorsToUpdate.map(({ previousMonitor, monitorWithRevision }) => ({
type: syntheticsMonitorType,
id: previousMonitor.id,
attributes: {
...monitorWithRevision,
[ConfigKey.CONFIG_ID]: previousMonitor.id,
[ConfigKey.MONITOR_QUERY_ID]:
monitorWithRevision[ConfigKey.CUSTOM_HEARTBEAT_ID] || previousMonitor.id,
},
}))
);
savedObjectsSuccessful = true;
return editedSOPromise;
} catch (e) {
savedObjectsSuccessful = false;
}
}
async function syncUpdatedMonitors() {
try {
const editSyncPromise = await syntheticsMonitorClient.editMonitors(
monitorsToUpdate.map(
({ normalizedMonitor, previousMonitor, decryptedPreviousMonitor }) => ({
monitor: {
...(normalizedMonitor as MonitorFields),
[ConfigKey.CONFIG_ID]: previousMonitor.id,
[ConfigKey.MONITOR_QUERY_ID]:
normalizedMonitor[ConfigKey.CUSTOM_HEARTBEAT_ID] || previousMonitor.id,
},
id: previousMonitor.id,
previousMonitor,
decryptedPreviousMonitor,
})
),
request,
savedObjectsClient,
privateLocations,
spaceId
);
syncSuccessful = true;
return editSyncPromise;
} catch (e) {
syncSuccessful = false;
}
}
const [editedMonitorSavedObjects, errors] = await Promise.all([
updateSavedObjects(),
syncUpdatedMonitors(),
const [editedMonitorSavedObjects, editSyncResponse] = await Promise.all([
updateConfigSavedObjects({ monitorsToUpdate, routeContext }),
syncUpdatedMonitors({ monitorsToUpdate, routeContext, spaceId, privateLocations }),
]);
const { failedPolicyUpdates, publicSyncErrors } = editSyncResponse;
monitorsToUpdate.forEach(({ normalizedMonitor, previousMonitor }) => {
const editedMonitorSavedObject = editedMonitorSavedObjects?.saved_objects.find(
(obj) => obj.id === previousMonitor.id
@ -121,25 +118,97 @@ export const syncEditedMonitorBulk = async ({
previousMonitor,
server.stackVersion,
Boolean((normalizedMonitor as MonitorFields)[ConfigKey.SOURCE_INLINE]),
errors
publicSyncErrors
)
);
});
return { errors, editedMonitors: editedMonitorSavedObjects?.saved_objects };
const failedConfigs = await rollbackFailedUpdates({
monitorsToUpdate,
routeContext,
failedPolicyUpdates,
});
return {
failedConfigs,
errors: publicSyncErrors,
editedMonitors: editedMonitorSavedObjects?.saved_objects,
};
} catch (e) {
server.logger.error(`Unable to update Synthetics monitors `);
if (!syncSuccessful && savedObjectsSuccessful) {
await savedObjectsClient.bulkUpdate<MonitorFields>(
monitorsToUpdate.map(({ previousMonitor, decryptedPreviousMonitor }) => ({
type: syntheticsMonitorType,
id: previousMonitor.id,
attributes: decryptedPreviousMonitor.attributes,
}))
);
}
server.logger.error(`Unable to update Synthetics monitors, ${e.message}`);
await rollbackCompletely({ routeContext, monitorsToUpdate });
throw e;
}
};
export const rollbackCompletely = async ({
routeContext,
monitorsToUpdate,
}: {
monitorsToUpdate: MonitorConfigUpdate[];
routeContext: RouteContext;
}) => {
const { savedObjectsClient, server } = routeContext;
try {
await savedObjectsClient.bulkUpdate<MonitorFields>(
monitorsToUpdate.map(({ previousMonitor, decryptedPreviousMonitor }) => ({
type: syntheticsMonitorType,
id: previousMonitor.id,
attributes: decryptedPreviousMonitor.attributes,
}))
);
} catch (e) {
server.logger.error(`Unable to rollback Synthetics monitors edit ${e.message} `);
}
};
export const rollbackFailedUpdates = async ({
routeContext,
failedPolicyUpdates,
monitorsToUpdate,
}: {
monitorsToUpdate: Array<{
previousMonitor: SavedObject<EncryptedSyntheticsMonitor>;
decryptedPreviousMonitor: SavedObject<SyntheticsMonitorWithSecrets>;
}>;
routeContext: RouteContext;
failedPolicyUpdates?: FailedPolicyUpdate[];
}) => {
if (!failedPolicyUpdates || failedPolicyUpdates.length === 0) {
return;
}
const { server, savedObjectsClient } = routeContext;
try {
const failedConfigs: Record<
string,
{ config: HeartbeatConfig; error?: Error | SavedObjectError }
> = {};
failedPolicyUpdates.forEach(({ config, error }) => {
if (config && config[ConfigKey.CONFIG_ID]) {
failedConfigs[config[ConfigKey.CONFIG_ID]] = {
config,
error,
};
}
});
const monitorsToRevert = monitorsToUpdate
.filter(({ previousMonitor }) => {
return failedConfigs[previousMonitor.id];
})
.map(({ previousMonitor, decryptedPreviousMonitor }) => ({
type: syntheticsMonitorType,
id: previousMonitor.id,
attributes: decryptedPreviousMonitor.attributes,
}));
if (monitorsToRevert.length > 0) {
await savedObjectsClient.bulkUpdate<MonitorFields>(monitorsToRevert);
}
return failedConfigs;
} catch (e) {
server.logger.error(`Unable to rollback Synthetics monitor failed updates, ${e.message} `);
}
};

View file

@ -5,14 +5,9 @@
* 2.0.
*/
import { schema } from '@kbn/config-schema';
import {
KibanaRequest,
SavedObjectsClientContract,
SavedObjectsErrorHelpers,
} from '@kbn/core/server';
import { SavedObjectsClientContract, SavedObjectsErrorHelpers } from '@kbn/core/server';
import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common';
import { deletePermissionError } from '../../synthetics_service/private_location/synthetics_private_location';
import { SyntheticsMonitorClient } from '../../synthetics_service/synthetics_monitor/synthetics_monitor_client';
import {
ConfigKey,
EncryptedSyntheticsMonitor,
@ -20,7 +15,7 @@ import {
SyntheticsMonitorWithId,
SyntheticsMonitorWithSecrets,
} from '../../../common/runtime_types';
import { SyntheticsRestApiRouteFactory } from '../../legacy_uptime/routes/types';
import { RouteContext, SyntheticsRestApiRouteFactory } from '../../legacy_uptime/routes/types';
import { API_URLS } from '../../../common/constants';
import { syntheticsMonitorType } from '../../legacy_uptime/lib/saved_objects/synthetics_monitor';
import { getMonitorNotFoundResponse } from '../synthetics_service/service_errors';
@ -41,22 +36,14 @@ export const deleteSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () =>
}),
},
writeAccess: true,
handler: async ({
request,
response,
savedObjectsClient,
server,
syntheticsMonitorClient,
}): Promise<any> => {
handler: async (routeContext): Promise<any> => {
const { request, response } = routeContext;
const { monitorId } = request.params;
try {
const errors = await deleteMonitor({
savedObjectsClient,
server,
routeContext,
monitorId,
syntheticsMonitorClient,
request,
});
if (errors && errors.length > 0) {
@ -77,18 +64,13 @@ export const deleteSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () =>
});
export const deleteMonitor = async ({
savedObjectsClient,
server,
routeContext,
monitorId,
syntheticsMonitorClient,
request,
}: {
savedObjectsClient: SavedObjectsClientContract;
server: UptimeServerSetup;
routeContext: RouteContext;
monitorId: string;
syntheticsMonitorClient: SyntheticsMonitorClient;
request: KibanaRequest;
}) => {
const { savedObjectsClient, server, syntheticsMonitorClient, request } = routeContext;
const { logger, telemetry, stackVersion } = server;
const { monitor, monitorWithSecret } = await getMonitorToDelete(
@ -125,7 +107,10 @@ export const deleteMonitor = async ({
);
const deletePromise = savedObjectsClient.delete(syntheticsMonitorType, monitorId);
const [errors] = await Promise.all([deleteSyncPromise, deletePromise]);
const [errors] = await Promise.all([deleteSyncPromise, deletePromise]).catch((e) => {
server.logger.error(e);
throw e;
});
sendTelemetryEvents(
logger,
@ -141,6 +126,10 @@ export const deleteMonitor = async ({
return errors;
} catch (e) {
server.logger.error(
`Unable to delete Synthetics monitor ${monitor.attributes[ConfigKey.NAME]}`
);
if (monitorWithSecret) {
await restoreDeletedMonitor({
monitorId,

View file

@ -98,11 +98,13 @@ describe('syncEditedMonitor', () => {
previousMonitor,
decryptedPreviousMonitor:
previousMonitor as unknown as SavedObject<SyntheticsMonitorWithSecrets>,
syntheticsMonitorClient,
server: serverMock,
request: {} as unknown as KibanaRequest,
savedObjectsClient:
serverMock.authSavedObjectsClient as unknown as SavedObjectsClientContract,
routeContext: {
syntheticsMonitorClient,
server: serverMock,
request: {} as unknown as KibanaRequest,
savedObjectsClient:
serverMock.authSavedObjectsClient as unknown as SavedObjectsClientContract,
} as any,
spaceId: 'test-space',
});

View file

@ -6,16 +6,10 @@
*/
import { mergeWith } from 'lodash';
import { schema } from '@kbn/config-schema';
import {
SavedObjectsUpdateResponse,
SavedObject,
SavedObjectsClientContract,
KibanaRequest,
} from '@kbn/core/server';
import { SavedObjectsUpdateResponse, SavedObject } from '@kbn/core/server';
import { SavedObjectsErrorHelpers } from '@kbn/core/server';
import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common';
import { getSyntheticsPrivateLocations } from '../../legacy_uptime/lib/saved_objects/private_locations';
import { SyntheticsMonitorClient } from '../../synthetics_service/synthetics_monitor/synthetics_monitor_client';
import {
MonitorFields,
EncryptedSyntheticsMonitor,
@ -23,7 +17,7 @@ import {
SyntheticsMonitor,
ConfigKey,
} from '../../../common/runtime_types';
import { SyntheticsRestApiRouteFactory } from '../../legacy_uptime/routes/types';
import { RouteContext, SyntheticsRestApiRouteFactory } from '../../legacy_uptime/routes/types';
import { API_URLS } from '../../../common/constants';
import { syntheticsMonitorType } from '../../legacy_uptime/lib/saved_objects/synthetics_monitor';
import { validateMonitor } from './monitor_validation';
@ -33,7 +27,6 @@ import {
formatTelemetryUpdateEvent,
} from '../telemetry/monitor_upgrade_sender';
import { formatSecrets, normalizeSecrets } from '../../synthetics_service/utils/secrets';
import type { UptimeServerSetup } from '../../legacy_uptime/lib/adapters/framework';
// Simplify return promise type and type it with runtime_types
export const editSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () => ({
@ -45,13 +38,8 @@ export const editSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () => (
}),
body: schema.any(),
},
handler: async ({
request,
response,
savedObjectsClient,
server,
syntheticsMonitorClient,
}): Promise<any> => {
handler: async (routeContext): Promise<any> => {
const { request, response, savedObjectsClient, server } = routeContext;
const { encryptedSavedObjects, logger } = server;
const encryptedSavedObjectsClient = encryptedSavedObjects.getClient();
const monitor = request.body as SyntheticsMonitor;
@ -96,21 +84,35 @@ export const editSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () => (
revision: (previousMonitor.attributes[ConfigKey.REVISION] || 0) + 1,
};
const { errors, editedMonitor: editedMonitorSavedObject } = await syncEditedMonitor({
server,
const {
publicSyncErrors,
failedPolicyUpdates,
editedMonitor: editedMonitorSavedObject,
} = await syncEditedMonitor({
routeContext,
previousMonitor,
decryptedPreviousMonitor,
syntheticsMonitorClient,
savedObjectsClient,
request,
normalizedMonitor: monitorWithRevision,
spaceId,
});
if (failedPolicyUpdates && failedPolicyUpdates.length > 0) {
const hasError = failedPolicyUpdates.find((update) => update.error);
await rollbackUpdate({
routeContext,
configId: monitorId,
attributes: decryptedPreviousMonitor.attributes,
});
throw hasError?.error;
}
// Return service sync errors in OK response
if (errors && errors.length > 0) {
if (publicSyncErrors && publicSyncErrors.length > 0) {
return response.ok({
body: { message: 'error pushing monitor to the service', attributes: { errors } },
body: {
message: 'error pushing monitor to the service',
attributes: { errors: publicSyncErrors },
},
});
}
@ -121,30 +123,45 @@ export const editSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () => (
}
logger.error(updateErr);
throw updateErr;
return response.customError({
body: { message: updateErr.message },
statusCode: 500,
});
}
},
});
const rollbackUpdate = async ({
routeContext,
configId,
attributes,
}: {
attributes: SyntheticsMonitorWithSecrets;
configId: string;
routeContext: RouteContext;
}) => {
const { savedObjectsClient, server } = routeContext;
try {
await savedObjectsClient.update<MonitorFields>(syntheticsMonitorType, configId, attributes);
} catch (e) {
server.logger.error(`Unable to rollback Synthetics monitors edit ${e.message} `);
}
};
export const syncEditedMonitor = async ({
normalizedMonitor,
previousMonitor,
decryptedPreviousMonitor,
server,
syntheticsMonitorClient,
savedObjectsClient,
request,
spaceId,
routeContext,
}: {
normalizedMonitor: SyntheticsMonitor;
previousMonitor: SavedObject<EncryptedSyntheticsMonitor>;
decryptedPreviousMonitor: SavedObject<SyntheticsMonitorWithSecrets>;
server: UptimeServerSetup;
syntheticsMonitorClient: SyntheticsMonitorClient;
savedObjectsClient: SavedObjectsClientContract;
request: KibanaRequest;
routeContext: RouteContext;
spaceId: string;
}) => {
const { server, savedObjectsClient, syntheticsMonitorClient } = routeContext;
try {
const monitorWithId = {
...normalizedMonitor,
@ -171,16 +188,17 @@ export const syncEditedMonitor = async ({
decryptedPreviousMonitor,
},
],
request,
savedObjectsClient,
routeContext,
allPrivateLocations,
spaceId
);
const [editedMonitorSavedObject, errors] = await Promise.all([
editedSOPromise,
editSyncPromise,
]);
const [editedMonitorSavedObject, { publicSyncErrors, failedPolicyUpdates }] = await Promise.all(
[editedSOPromise, editSyncPromise]
).catch((e) => {
server.logger.error(e);
throw e;
});
sendTelemetryEvents(
server.logger,
@ -190,20 +208,24 @@ export const syncEditedMonitor = async ({
previousMonitor,
server.stackVersion,
Boolean((normalizedMonitor as MonitorFields)[ConfigKey.SOURCE_INLINE]),
errors
publicSyncErrors
)
);
return { errors, editedMonitor: editedMonitorSavedObject };
return {
failedPolicyUpdates,
publicSyncErrors,
editedMonitor: editedMonitorSavedObject,
};
} catch (e) {
server.logger.error(
`Unable to update Synthetics monitor ${decryptedPreviousMonitor.attributes[ConfigKey.NAME]}`
);
await savedObjectsClient.update<MonitorFields>(
syntheticsMonitorType,
previousMonitor.id,
decryptedPreviousMonitor.attributes
);
await rollbackUpdate({
routeContext,
configId: previousMonitor.id,
attributes: decryptedPreviousMonitor.attributes,
});
throw e;
}

View file

@ -25,11 +25,6 @@ import {
} from '../../legacy_uptime/lib/telemetry/constants';
import { MonitorErrorEvent } from '../../legacy_uptime/lib/telemetry/types';
export interface UpgradeError {
key?: string;
message: string | string[];
}
export function sendTelemetryEvents(
logger: Logger,
eventsTelemetry: TelemetryEventsSender | undefined,

View file

@ -5,6 +5,7 @@
* 2.0.
*/
import { KibanaResponse } from '@kbn/core-http-router-server-internal';
import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common';
import { checkIndicesReadPrivileges } from './synthetics_service/authentication/check_has_privilege';
import { SYNTHETICS_INDEX_PATTERN } from '../common/constants';
import { isTestUser, UptimeEsClient } from './legacy_uptime/lib/lib';
@ -39,6 +40,8 @@ export const syntheticsRouteWrapper: SyntheticsRouteWrapper = (
server.uptimeEsClient = uptimeEsClient;
const spaceId = server.spaces?.spacesService.getSpaceId(request) ?? DEFAULT_SPACE_ID;
const res = await (uptimeRoute.handler as SyntheticsStreamingRouteHandler)({
uptimeEsClient,
savedObjectsClient,
@ -47,6 +50,7 @@ export const syntheticsRouteWrapper: SyntheticsRouteWrapper = (
server,
syntheticsMonitorClient,
subject,
spaceId,
});
return res;
@ -71,6 +75,8 @@ export const syntheticsRouteWrapper: SyntheticsRouteWrapper = (
server.uptimeEsClient = uptimeEsClient;
const spaceId = server.spaces?.spacesService.getSpaceId(request) ?? DEFAULT_SPACE_ID;
try {
const res = await uptimeRoute.handler({
uptimeEsClient,
@ -79,6 +85,7 @@ export const syntheticsRouteWrapper: SyntheticsRouteWrapper = (
request,
response,
server,
spaceId,
syntheticsMonitorClient,
});
if (res instanceof KibanaResponse) {

View file

@ -107,7 +107,7 @@ describe('SyntheticsPrivateLocation', () => {
});
try {
await syntheticsPrivateLocation.createMonitors(
await syntheticsPrivateLocation.createPackagePolicies(
[{ config: testConfig, globalParams: {} }],
{} as unknown as KibanaRequest,
savedObjectsClientMock,

View file

@ -8,6 +8,7 @@ import { KibanaRequest, SavedObjectsClientContract } from '@kbn/core/server';
import { NewPackagePolicy } from '@kbn/fleet-plugin/common';
import { NewPackagePolicyWithId } from '@kbn/fleet-plugin/server/services/package_policy';
import { cloneDeep } from 'lodash';
import { SavedObjectError } from '@kbn/core-saved-objects-common';
import { formatSyntheticsPolicy } from '../../../common/formatters/format_synthetics_policy';
import {
ConfigKey,
@ -18,6 +19,17 @@ import {
} from '../../../common/runtime_types';
import { UptimeServerSetup } from '../../legacy_uptime/lib/adapters';
export interface PrivateConfig {
config: HeartbeatConfig;
globalParams: Record<string, string>;
}
export interface FailedPolicyUpdate {
packagePolicy: NewPackagePolicyWithId;
config?: HeartbeatConfig;
error?: Error | SavedObjectError;
}
export class SyntheticsPrivateLocation {
private readonly server: UptimeServerSetup;
@ -49,7 +61,7 @@ export class SyntheticsPrivateLocation {
newPolicyTemplate: NewPackagePolicy,
spaceId: string,
globalParams: Record<string, string>
): NewPackagePolicy | null {
): (NewPackagePolicy & { policy_id: string }) | null {
if (!savedObjectsClient) {
throw new Error('Could not find savedObjectsClient');
}
@ -98,13 +110,16 @@ export class SyntheticsPrivateLocation {
}
}
async createMonitors(
configs: Array<{ config: HeartbeatConfig; globalParams: Record<string, string> }>,
async createPackagePolicies(
configs: PrivateConfig[],
request: KibanaRequest,
savedObjectsClient: SavedObjectsClientContract,
privateLocations: PrivateLocation[],
spaceId: string
) {
if (configs.length === 0) {
return { created: [], failed: [] };
}
await this.checkPermissions(
request,
`Unable to create Synthetics package policy for monitor. Fleet write permissions are needed to use Synthetics private locations.`
@ -155,6 +170,7 @@ export class SyntheticsPrivateLocation {
}
} catch (e) {
this.server.logger.error(e);
throw e;
}
}
@ -166,6 +182,7 @@ export class SyntheticsPrivateLocation {
return await this.createPolicyBulk(newPolicies, savedObjectsClient);
} catch (e) {
this.server.logger.error(e);
throw e;
}
}
@ -177,7 +194,7 @@ export class SyntheticsPrivateLocation {
spaceId: string
) {
if (configs.length === 0) {
return;
return {};
}
await this.checkPermissions(
@ -191,7 +208,7 @@ export class SyntheticsPrivateLocation {
throw new Error(`Unable to create Synthetics package policy for private location`);
}
const policiesToUpdate: Array<NewPackagePolicy & { version?: string; id: string }> = [];
const policiesToUpdate: NewPackagePolicyWithId[] = [];
const policiesToCreate: NewPackagePolicyWithId[] = [];
const policiesToDelete: string[] = [];
@ -223,37 +240,50 @@ export class SyntheticsPrivateLocation {
);
if (!newPolicy) {
throw new Error(
`Unable to ${
hasPolicy ? 'update' : 'create'
} Synthetics package policy for private location ${privateLocation.label}`
);
throwAddEditError(hasPolicy, privateLocation.label);
}
if (hasPolicy) {
policiesToUpdate.push({ ...newPolicy, id: currId });
policiesToUpdate.push({ ...newPolicy, id: currId } as NewPackagePolicyWithId);
} else {
policiesToCreate.push({ ...newPolicy, id: currId });
policiesToCreate.push({ ...newPolicy, id: currId } as NewPackagePolicyWithId);
}
} else if (hasPolicy) {
policiesToDelete.push(currId);
}
} catch (e) {
this.server.logger.error(e);
throw new Error(
`Unable to ${hasPolicy ? 'update' : 'create'} Synthetics package policy for monitor ${
config[ConfigKey.NAME]
} with private location ${privateLocation.label}`
);
throwAddEditError(hasPolicy, privateLocation.label, config[ConfigKey.NAME]);
}
}
}
await Promise.all([
const [_createResponse, failedUpdatesRes, _deleteResponse] = await Promise.all([
this.createPolicyBulk(policiesToCreate, savedObjectsClient),
this.updatePolicyBulk(policiesToUpdate, savedObjectsClient),
this.deletePolicyBulk(policiesToDelete, savedObjectsClient),
]);
const failedUpdates = failedUpdatesRes?.map(({ packagePolicy, error }) => {
const policyConfig = configs.find(({ config }) => {
const { locations } = config;
const monitorPrivateLocations = locations.filter((loc) => !loc.isServiceManaged);
for (const privateLocation of monitorPrivateLocations) {
const currId = this.getPolicyId(config, privateLocation.id, spaceId);
return currId === packagePolicy.id;
}
});
return {
error,
packagePolicy,
config: policyConfig?.config,
};
});
return {
failedUpdates,
};
}
async getExistingPolicies(
@ -292,20 +322,21 @@ export class SyntheticsPrivateLocation {
}
async updatePolicyBulk(
updatedPolicies: Array<NewPackagePolicy & { version?: string; id: string }>,
policiesToUpdate: NewPackagePolicyWithId[],
savedObjectsClient: SavedObjectsClientContract
) {
const soClient = savedObjectsClient;
const esClient = this.server.uptimeEsClient.baseESClient;
if (soClient && esClient && updatedPolicies.length > 0) {
return await this.server.fleet.packagePolicyService.bulkUpdate(
if (soClient && esClient && policiesToUpdate.length > 0) {
const { failedPolicies } = await this.server.fleet.packagePolicyService.bulkUpdate(
soClient,
esClient,
updatedPolicies,
policiesToUpdate,
{
force: true,
}
);
return failedPolicies;
}
}
@ -368,6 +399,14 @@ export class SyntheticsPrivateLocation {
}
}
const throwAddEditError = (hasPolicy: boolean, location?: string, name?: string) => {
throw new Error(
`Unable to ${hasPolicy ? 'update' : 'create'} Synthetics package policy ${
name ? 'for monitor ' + name : ''
} for private location: ${location}`
);
};
export const deletePermissionError = (name?: string) => {
return `Unable to delete Synthetics package policy for monitor ${name}. Fleet write permissions are needed to use Synthetics private locations.`;
};

View file

@ -127,7 +127,7 @@ describe('ProjectMonitorFormatter', () => {
const syntheticsService = new SyntheticsService(serverMock);
syntheticsService.addConfig = jest.fn();
syntheticsService.addConfigs = jest.fn();
syntheticsService.editConfig = jest.fn();
syntheticsService.deleteConfigs = jest.fn();
@ -149,6 +149,13 @@ describe('ProjectMonitorFormatter', () => {
const monitorClient = new SyntheticsMonitorClient(syntheticsService, serverMock);
const routeContext = {
savedObjectsClient: soClient,
server: serverMock,
syntheticsMonitorClient: monitorClient,
request: kibanaRequest,
} as any;
jest.spyOn(locationsUtil, 'getAllLocations').mockImplementation(
async () =>
({
@ -161,12 +168,9 @@ describe('ProjectMonitorFormatter', () => {
const pushMonitorFormatter = new ProjectMonitorFormatter({
projectId: 'test-project',
spaceId: 'default-space',
routeContext,
encryptedSavedObjectsClient,
savedObjectsClient: soClient,
monitors: testMonitors,
server: serverMock,
syntheticsMonitorClient: monitorClient,
request: kibanaRequest,
});
pushMonitorFormatter.getProjectMonitorsForProject = jest.fn().mockResolvedValue([]);
@ -210,11 +214,8 @@ describe('ProjectMonitorFormatter', () => {
projectId: 'test-project',
spaceId: 'default-space',
encryptedSavedObjectsClient,
savedObjectsClient: soClient,
routeContext,
monitors: testMonitors,
server: serverMock,
syntheticsMonitorClient: monitorClient,
request: kibanaRequest,
});
pushMonitorFormatter.getProjectMonitorsForProject = jest.fn().mockResolvedValue([]);
@ -254,15 +255,18 @@ describe('ProjectMonitorFormatter', () => {
},
} as any;
soClient.bulkCreate.mockImplementation(async () => {
return {
saved_objects: [],
};
});
const pushMonitorFormatter = new ProjectMonitorFormatter({
projectId: 'test-project',
spaceId: 'default-space',
encryptedSavedObjectsClient,
savedObjectsClient: soClient,
monitors: testMonitors,
server: serverMock,
syntheticsMonitorClient: monitorClient,
request: kibanaRequest,
routeContext,
});
pushMonitorFormatter.getProjectMonitorsForProject = jest.fn().mockResolvedValue([]);
@ -301,11 +305,8 @@ describe('ProjectMonitorFormatter', () => {
projectId: 'test-project',
spaceId: 'default-space',
encryptedSavedObjectsClient,
savedObjectsClient: soClient,
monitors: testMonitors,
server: serverMock,
syntheticsMonitorClient: monitorClient,
request: kibanaRequest,
routeContext,
});
pushMonitorFormatter.getProjectMonitorsForProject = jest.fn().mockResolvedValue([]);
@ -345,11 +346,8 @@ describe('ProjectMonitorFormatter', () => {
projectId: 'test-project',
spaceId: 'default-space',
encryptedSavedObjectsClient,
savedObjectsClient: soClient,
monitors: testMonitors,
server: serverMock,
syntheticsMonitorClient: monitorClient,
request: kibanaRequest,
routeContext,
});
pushMonitorFormatter.getProjectMonitorsForProject = jest.fn().mockResolvedValue([]);
@ -385,7 +383,7 @@ describe('ProjectMonitorFormatter', () => {
soClient.bulkCreate = jest.fn().mockResolvedValue({ saved_objects: soResult });
monitorClient.addMonitors = jest.fn().mockReturnValue({});
monitorClient.addMonitors = jest.fn().mockReturnValue([]);
const telemetrySpy = jest
.spyOn(telemetryHooks, 'sendTelemetryEvents')
@ -395,11 +393,8 @@ describe('ProjectMonitorFormatter', () => {
projectId: 'test-project',
spaceId: 'default-space',
encryptedSavedObjectsClient,
savedObjectsClient: soClient,
monitors: testMonitors,
server: serverMock,
syntheticsMonitorClient: monitorClient,
request: kibanaRequest,
routeContext,
});
pushMonitorFormatter.getProjectMonitorsForProject = jest.fn().mockResolvedValue([]);

View file

@ -13,6 +13,7 @@ import {
} from '@kbn/core/server';
import { i18n } from '@kbn/i18n';
import { EncryptedSavedObjectsClient } from '@kbn/encrypted-saved-objects-plugin/server';
import { RouteContext } from '../../legacy_uptime/routes';
import { getAllLocations } from '../get_all_locations';
import { syncNewMonitorBulk } from '../../routes/monitor_cruds/bulk_cruds/add_monitor_bulk';
import { SyntheticsMonitorClient } from '../synthetics_monitor/synthetics_monitor_client';
@ -62,13 +63,6 @@ export const FAILED_TO_UPDATE_MONITOR = i18n.translate(
}
);
export const FAILED_TO_UPDATE_MONITORS = i18n.translate(
'xpack.synthetics.service.projectMonitors.failedToUpdateMonitors',
{
defaultMessage: 'Failed to create or update monitors',
}
);
export class ProjectMonitorFormatter {
private projectId: string;
private spaceId: string;
@ -84,37 +78,33 @@ export class ProjectMonitorFormatter {
private projectFilter: string;
private syntheticsMonitorClient: SyntheticsMonitorClient;
private request: KibanaRequest;
private routeContext: RouteContext;
private writeIntegrationPoliciesPermissions?: boolean;
constructor({
savedObjectsClient,
encryptedSavedObjectsClient,
projectId,
spaceId,
monitors,
server,
syntheticsMonitorClient,
request,
routeContext,
}: {
savedObjectsClient: SavedObjectsClientContract;
routeContext: RouteContext;
encryptedSavedObjectsClient: EncryptedSavedObjectsClient;
projectId: string;
spaceId: string;
monitors: ProjectMonitor[];
server: UptimeServerSetup;
syntheticsMonitorClient: SyntheticsMonitorClient;
request: KibanaRequest;
}) {
this.routeContext = routeContext;
this.projectId = projectId;
this.spaceId = spaceId;
this.savedObjectsClient = savedObjectsClient;
this.savedObjectsClient = routeContext.savedObjectsClient;
this.encryptedSavedObjectsClient = encryptedSavedObjectsClient;
this.syntheticsMonitorClient = syntheticsMonitorClient;
this.syntheticsMonitorClient = routeContext.syntheticsMonitorClient;
this.monitors = monitors;
this.server = server;
this.server = routeContext.server;
this.projectFilter = `${syntheticsMonitorType}.attributes.${ConfigKey.PROJECT_ID}: "${this.projectId}"`;
this.request = request;
this.request = routeContext.request;
this.publicLocations = [];
this.privateLocations = [];
}
@ -190,9 +180,10 @@ export class ProjectMonitorFormatter {
}
}
await this.createMonitorsBulk(normalizedNewMonitors);
await this.updateMonitorsBulk(normalizedUpdateMonitors);
await Promise.allSettled([
this.createMonitorsBulk(normalizedNewMonitors),
this.updateMonitorsBulk(normalizedUpdateMonitors),
]);
};
validatePermissions = async ({ monitor }: { monitor: ProjectMonitor }) => {
@ -298,33 +289,53 @@ export class ProjectMonitorFormatter {
private createMonitorsBulk = async (monitors: SyntheticsMonitor[]) => {
try {
if (monitors.length > 0) {
const { newMonitors } = await syncNewMonitorBulk({
const { newMonitors, failedMonitors } = await syncNewMonitorBulk({
normalizedMonitors: monitors,
server: this.server,
syntheticsMonitorClient: this.syntheticsMonitorClient,
soClient: this.savedObjectsClient,
request: this.request,
routeContext: this.routeContext,
privateLocations: this.privateLocations,
spaceId: this.spaceId,
});
if (newMonitors && newMonitors.length === monitors.length) {
this.createdMonitors.push(...monitors.map((monitor) => monitor[ConfigKey.JOURNEY_ID]!));
} else {
if (newMonitors.length > 0) {
newMonitors.forEach((monitor) => {
const journeyId = monitor.attributes[ConfigKey.JOURNEY_ID];
if (journeyId && !monitor.error) {
this.createdMonitors.push(journeyId);
} else if (monitor.error) {
this.failedMonitors.push({
reason: i18n.translate(
'xpack.synthetics.service.projectMonitors.failedToCreateMonitors',
{
defaultMessage: 'Failed to create monitor: {journeyId}',
values: {
journeyId,
},
}
),
details: monitor.error.message,
payload: monitor,
});
}
});
}
failedMonitors.forEach(({ monitor, error }) => {
const journeyId = monitor.attributes[ConfigKey.JOURNEY_ID];
this.failedMonitors.push({
reason: i18n.translate(
'xpack.synthetics.service.projectMonitors.failedToCreateXMonitors',
reason: error?.message ?? FAILED_TO_UPDATE_MONITOR,
details: i18n.translate(
'xpack.synthetics.service.projectMonitors.failedToCreateMonitors',
{
defaultMessage: 'Failed to create {length} monitors',
defaultMessage: 'Failed to create monitor: {journeyId}',
values: {
length: monitors.length,
journeyId,
},
}
),
details: FAILED_TO_UPDATE_MONITORS,
payload: monitors,
});
}
});
}
} catch (e) {
this.server.logger.error(e);
@ -363,60 +374,103 @@ export class ProjectMonitorFormatter {
monitor: SyntheticsMonitor;
previousMonitor: SavedObjectsFindResult<EncryptedSyntheticsMonitor>;
}>
): Promise<{
editedMonitors: Array<SavedObjectsUpdateResponse<EncryptedSyntheticsMonitor>>;
errors: ServiceLocationErrors;
updatedCount: number;
}> => {
if (monitors.length === 0) {
): Promise<
| {
editedMonitors: Array<SavedObjectsUpdateResponse<EncryptedSyntheticsMonitor>>;
errors: ServiceLocationErrors;
updatedCount: number;
}
| undefined
> => {
try {
if (monitors.length === 0) {
return {
editedMonitors: [],
errors: [],
updatedCount: 0,
};
}
const decryptedPreviousMonitors = await this.getDecryptedMonitors(
monitors.map((m) => m.previousMonitor)
);
const monitorsToUpdate = [];
for (let i = 0; i < decryptedPreviousMonitors.length; i++) {
const decryptedPreviousMonitor = decryptedPreviousMonitors[i];
const previousMonitor = monitors[i].previousMonitor;
const normalizedMonitor = monitors[i].monitor;
const {
attributes: { [ConfigKey.REVISION]: _, ...normalizedPreviousMonitorAttributes },
} = normalizeSecrets(decryptedPreviousMonitor);
const monitorWithRevision = formatSecrets({
...normalizedPreviousMonitorAttributes,
...normalizedMonitor,
revision: (previousMonitor.attributes[ConfigKey.REVISION] || 0) + 1,
});
monitorsToUpdate.push({
normalizedMonitor,
previousMonitor,
monitorWithRevision,
decryptedPreviousMonitor,
});
}
const { editedMonitors, failedConfigs } = await syncEditedMonitorBulk({
monitorsToUpdate,
routeContext: this.routeContext,
privateLocations: this.privateLocations,
spaceId: this.spaceId,
});
if (failedConfigs && Object.keys(failedConfigs).length > 0) {
const failedConfigsIds = Object.keys(failedConfigs);
failedConfigsIds.forEach((id) => {
const { config, error } = failedConfigs[id];
const journeyId = config[ConfigKey.JOURNEY_ID];
this.failedMonitors.push({
reason: error?.message ?? FAILED_TO_UPDATE_MONITOR,
details: i18n.translate(
'xpack.synthetics.service.projectMonitors.failedToUpdateJourney',
{
defaultMessage: 'Failed to update journey: {journeyId}',
values: {
journeyId,
},
}
),
payload: config,
});
});
// remove failed monitors from the list of updated monitors
this.updatedMonitors.splice(
this.updatedMonitors.findIndex((monitorId) => failedConfigsIds.includes(monitorId)),
failedConfigsIds.length
);
}
return {
editedMonitors: [],
errors: [],
updatedCount: 0,
editedMonitors: editedMonitors ?? [],
updatedCount: monitorsToUpdate.length,
};
}
const decryptedPreviousMonitors = await this.getDecryptedMonitors(
monitors.map((m) => m.previousMonitor)
);
const monitorsToUpdate = [];
for (let i = 0; i < decryptedPreviousMonitors.length; i++) {
const decryptedPreviousMonitor = decryptedPreviousMonitors[i];
const previousMonitor = monitors[i].previousMonitor;
const normalizedMonitor = monitors[i].monitor;
const {
attributes: { [ConfigKey.REVISION]: _, ...normalizedPreviousMonitorAttributes },
} = normalizeSecrets(decryptedPreviousMonitor);
const monitorWithRevision = formatSecrets({
...normalizedPreviousMonitorAttributes,
...normalizedMonitor,
revision: (previousMonitor.attributes[ConfigKey.REVISION] || 0) + 1,
});
monitorsToUpdate.push({
normalizedMonitor,
previousMonitor,
monitorWithRevision,
decryptedPreviousMonitor,
} catch (e) {
this.server.logger.error(e);
this.failedMonitors.push({
reason: i18n.translate('xpack.synthetics.service.projectMonitors.failedToUpdateXMonitors', {
defaultMessage: 'Failed to update {length} monitors',
values: {
length: monitors.length,
},
}),
details: e.message,
payload: monitors,
});
}
const { editedMonitors } = await syncEditedMonitorBulk({
monitorsToUpdate,
server: this.server,
syntheticsMonitorClient: this.syntheticsMonitorClient,
savedObjectsClient: this.savedObjectsClient,
request: this.request,
privateLocations: this.privateLocations,
spaceId: this.spaceId,
});
return {
editedMonitors: editedMonitors ?? [],
errors: [],
updatedCount: monitorsToUpdate.length,
};
};
private validateMonitor = ({

View file

@ -69,7 +69,7 @@ describe('SyntheticsMonitorClient', () => {
const syntheticsService = new SyntheticsService(serverMock);
syntheticsService.addConfig = jest.fn();
syntheticsService.addConfigs = jest.fn();
syntheticsService.editConfig = jest.fn();
syntheticsService.deleteConfigs = jest.fn();
@ -129,7 +129,7 @@ describe('SyntheticsMonitorClient', () => {
const id = 'test-id-1';
const client = new SyntheticsMonitorClient(syntheticsService, serverMock);
client.privateLocationAPI.createMonitors = jest.fn();
client.privateLocationAPI.createPackagePolicies = jest.fn();
await client.addMonitors(
[{ monitor, id }],
@ -139,8 +139,8 @@ describe('SyntheticsMonitorClient', () => {
'test-space'
);
expect(syntheticsService.addConfig).toHaveBeenCalledTimes(1);
expect(client.privateLocationAPI.createMonitors).toHaveBeenCalledTimes(1);
expect(syntheticsService.addConfigs).toHaveBeenCalledTimes(1);
expect(client.privateLocationAPI.createPackagePolicies).toHaveBeenCalledTimes(1);
});
it('should edit a monitor', async () => {
@ -148,7 +148,7 @@ describe('SyntheticsMonitorClient', () => {
const id = 'test-id-1';
const client = new SyntheticsMonitorClient(syntheticsService, serverMock);
client.privateLocationAPI.editMonitors = jest.fn();
client.privateLocationAPI.editMonitors = jest.fn().mockResolvedValue({});
await client.editMonitors(
[
@ -159,8 +159,10 @@ describe('SyntheticsMonitorClient', () => {
decryptedPreviousMonitor: previousMonitor,
},
],
mockRequest,
savedObjectsClientMock,
{
request: mockRequest,
savedObjectsClient: savedObjectsClientMock,
} as any,
privateLocations,
'test-space'
);
@ -175,7 +177,7 @@ describe('SyntheticsMonitorClient', () => {
const id = 'test-id-1';
const client = new SyntheticsMonitorClient(syntheticsService, serverMock);
syntheticsService.editConfig = jest.fn();
client.privateLocationAPI.editMonitors = jest.fn();
client.privateLocationAPI.editMonitors = jest.fn().mockResolvedValue({});
monitor.locations = previousMonitor.attributes.locations.filter(
(loc: any) => loc.id !== locations[0].id
@ -190,8 +192,10 @@ describe('SyntheticsMonitorClient', () => {
decryptedPreviousMonitor: previousMonitor,
},
],
mockRequest,
savedObjectsClientMock,
{
request: mockRequest,
savedObjectsClient: savedObjectsClientMock,
} as any,
privateLocations,
'test-space'
);

View file

@ -11,9 +11,13 @@ import {
SavedObjectsFindResult,
} from '@kbn/core/server';
import { EncryptedSavedObjectsPluginStart } from '@kbn/encrypted-saved-objects-plugin/server';
import { RouteContext } from '../../legacy_uptime/routes';
import { normalizeSecrets } from '../utils';
import { UptimeServerSetup } from '../../legacy_uptime/lib/adapters';
import { SyntheticsPrivateLocation } from '../private_location/synthetics_private_location';
import {
PrivateConfig,
SyntheticsPrivateLocation,
} from '../private_location/synthetics_private_location';
import { SyntheticsService } from '../synthetics_service';
import {
ConfigData,
@ -47,8 +51,7 @@ export class SyntheticsMonitorClient {
allPrivateLocations: PrivateLocation[],
spaceId: string
) {
const privateConfigs: Array<{ config: HeartbeatConfig; globalParams: Record<string, string> }> =
[];
const privateConfigs: PrivateConfig[] = [];
const publicConfigs: ConfigData[] = [];
const paramsBySpace = await this.syntheticsService.getSyntheticsParams({ spaceId });
@ -78,25 +81,17 @@ export class SyntheticsMonitorClient {
}
}
let newPolicies;
const newPolicies = this.privateLocationAPI.createPackagePolicies(
privateConfigs,
request,
savedObjectsClient,
allPrivateLocations,
spaceId
);
if (privateConfigs.length > 0) {
newPolicies = await this.privateLocationAPI.createMonitors(
privateConfigs,
request,
savedObjectsClient,
allPrivateLocations,
spaceId
);
}
const syncErrors = this.syntheticsService.addConfigs(publicConfigs);
let syncErrors;
if (publicConfigs.length > 0) {
syncErrors = await this.syntheticsService.addConfig(publicConfigs);
}
return { newPolicies, syncErrors };
return await Promise.all([newPolicies, syncErrors]);
}
async editMonitors(
@ -106,11 +101,11 @@ export class SyntheticsMonitorClient {
previousMonitor: SavedObject<EncryptedSyntheticsMonitor>;
decryptedPreviousMonitor: SavedObject<SyntheticsMonitorWithSecrets>;
}>,
request: KibanaRequest,
savedObjectsClient: SavedObjectsClientContract,
routeContext: RouteContext,
allPrivateLocations: PrivateLocation[],
spaceId: string
) {
const { request, savedObjectsClient } = routeContext;
const privateConfigs: Array<{ config: HeartbeatConfig; globalParams: Record<string, string> }> =
[];
@ -151,7 +146,11 @@ export class SyntheticsMonitorClient {
}
}
await this.privateLocationAPI.editMonitors(
if (deletedPublicConfigs.length > 0) {
await this.syntheticsService.deleteConfigs(deletedPublicConfigs);
}
const privateEditPromise = this.privateLocationAPI.editMonitors(
privateConfigs,
request,
savedObjectsClient,
@ -159,13 +158,16 @@ export class SyntheticsMonitorClient {
spaceId
);
if (deletedPublicConfigs.length > 0) {
await this.syntheticsService.deleteConfigs(deletedPublicConfigs);
}
const publicConfigsPromise = this.syntheticsService.editConfig(publicConfigs);
if (publicConfigs.length > 0) {
return await this.syntheticsService.editConfig(publicConfigs);
}
const [publicSyncErrors, privateEditResponse] = await Promise.all([
publicConfigsPromise,
privateEditPromise,
]);
const { failedUpdates: failedPolicyUpdates } = privateEditResponse;
return { failedPolicyUpdates, publicSyncErrors };
}
async deleteMonitors(
monitors: SyntheticsMonitorWithId[],

View file

@ -195,7 +195,7 @@ describe('SyntheticsService', () => {
(axios as jest.MockedFunction<typeof axios>).mockResolvedValue({} as AxiosResponse);
await service.addConfig({ monitor: payload } as any);
await service.addConfigs({ monitor: payload } as any);
expect(axios).toHaveBeenCalledTimes(1);
expect(axios).toHaveBeenCalledWith(
@ -291,7 +291,7 @@ describe('SyntheticsService', () => {
const payload = getFakePayload([locations[0]]);
await service.addConfig({ monitor: payload } as any);
await service.addConfigs({ monitor: payload } as any);
expect(axios).toHaveBeenCalledTimes(1);
expect(axios).toHaveBeenCalledWith(

View file

@ -299,9 +299,13 @@ export class SyntheticsService {
};
}
async addConfig(config: ConfigData | ConfigData[]) {
async addConfigs(configs: ConfigData[]) {
try {
const monitors = this.formatConfigs(Array.isArray(config) ? config : [config]);
if (configs.length === 0) {
return;
}
const monitors = this.formatConfigs(configs);
const license = await this.getLicense();
const output = await this.getOutput();
@ -320,12 +324,13 @@ export class SyntheticsService {
}
}
async editConfig(monitorConfig: ConfigData | ConfigData[], isEdit = true) {
async editConfig(monitorConfig: ConfigData[], isEdit = true) {
try {
if (monitorConfig.length === 0) {
return;
}
const license = await this.getLicense();
const monitors = this.formatConfigs(
Array.isArray(monitorConfig) ? monitorConfig : [monitorConfig]
);
const monitors = this.formatConfigs(monitorConfig);
const output = await this.getOutput();
if (output) {
@ -419,23 +424,30 @@ export class SyntheticsService {
}
async deleteConfigs(configs: ConfigData[]) {
const license = await this.getLicense();
const hasPublicLocations = configs.some((config) =>
config.monitor.locations.some(({ isServiceManaged }) => isServiceManaged)
);
if (hasPublicLocations) {
const output = await this.getOutput();
if (!output) {
try {
if (configs.length === 0) {
return;
}
const license = await this.getLicense();
const hasPublicLocations = configs.some((config) =>
config.monitor.locations.some(({ isServiceManaged }) => isServiceManaged)
);
const data = {
output,
monitors: this.formatConfigs(configs),
license,
};
return await this.apiClient.delete(data);
if (hasPublicLocations) {
const output = await this.getOutput();
if (!output) {
return;
}
const data = {
output,
monitors: this.formatConfigs(configs),
license,
};
return await this.apiClient.delete(data);
}
} catch (e) {
this.server.logger.error(e);
}
}

View file

@ -34986,7 +34986,6 @@
"xpack.synthetics.server.project.delete.toolarge": "La charge utile de la requête de suppression est trop volumineuse. Veuillez envoyer au maximum 250 moniteurs à supprimer par requête",
"xpack.synthetics.service.projectMonitors.cannotUpdateMonitorToDifferentType": "Impossible de mettre à jour le moniteur avec un type différent.",
"xpack.synthetics.service.projectMonitors.failedToUpdateMonitor": "Impossible de créer ou de mettre à jour le moniteur",
"xpack.synthetics.service.projectMonitors.failedToUpdateMonitors": "Impossible de créer ou de mettre à jour les moniteurs",
"xpack.synthetics.service.projectMonitors.insufficientFleetPermissions": "Permissions insuffisantes. Pour configurer les emplacements privés, vous devez disposer d'autorisations d'écriture sur Fleet et sur les intégrations. Pour résoudre ce problème, veuillez générer une nouvelle clé d'API avec un utilisateur disposant des autorisations d'écriture sur Fleet et sur les intégrations.",
"xpack.synthetics.settings.alertDefaultForm.requiredEmail": "À : L'e-mail est requis pour le connecteur d'e-mails sélectionné",
"xpack.synthetics.settings.applyChanges": "Appliquer les modifications",

View file

@ -34965,7 +34965,6 @@
"xpack.synthetics.server.project.delete.toolarge": "削除リクエストペイロードが大きすぎます。リクエストごとに送信できる削除するモニターは250以下にしてください",
"xpack.synthetics.service.projectMonitors.cannotUpdateMonitorToDifferentType": "モニターを別のタイプに更新できません。",
"xpack.synthetics.service.projectMonitors.failedToUpdateMonitor": "モニターを作成または更新できません",
"xpack.synthetics.service.projectMonitors.failedToUpdateMonitors": "モニターを作成または更新できません",
"xpack.synthetics.service.projectMonitors.insufficientFleetPermissions": "パーミッションがありません。非公開の場所を構成するには、Fleetと統合の書き込み権限が必要です。解決するには、Fleetと統合の書き込み権限が割り当てられたユーザーで、新しいAPIキーを生成してください。",
"xpack.synthetics.settings.alertDefaultForm.requiredEmail": "終了:選択した電子メールコネクターには電子メールアドレスが必須です",
"xpack.synthetics.settings.applyChanges": "変更を適用",

View file

@ -34982,7 +34982,6 @@
"xpack.synthetics.server.project.delete.toolarge": "删除请求,有效负载太大。每个请求请最多发送 250 个要删除的监测",
"xpack.synthetics.service.projectMonitors.cannotUpdateMonitorToDifferentType": "无法将监测更新为不同类型。",
"xpack.synthetics.service.projectMonitors.failedToUpdateMonitor": "无法创建或更新监测",
"xpack.synthetics.service.projectMonitors.failedToUpdateMonitors": "无法创建或更新监测",
"xpack.synthetics.service.projectMonitors.insufficientFleetPermissions": "权限不足。要配置专用位置,您必须具有 Fleet 和集成写入权限。要解决问题,请通过具有 Fleet 和集成写入权限的用户生成新的 API 密钥。",
"xpack.synthetics.settings.alertDefaultForm.requiredEmail": "到:选定的电子邮件连接器需要电子邮件",
"xpack.synthetics.settings.applyChanges": "应用更改",

View file

@ -272,7 +272,7 @@ export default function ({ getService }: FtrProviderContext) {
.send(httpMonitorJson);
expect(apiResponse.status).eql(403);
expect(apiResponse.body.message).eql('Unable to create synthetics-monitor');
expect(apiResponse.body.message).eql('Forbidden');
});
});

View file

@ -13,13 +13,14 @@ import { omit } from 'lodash';
import { secretKeys } from '@kbn/synthetics-plugin/common/constants/monitor_management';
import { PackagePolicy } from '@kbn/fleet-plugin/common';
import expect from '@kbn/expect';
import { syntheticsMonitorType } from '@kbn/synthetics-plugin/server/legacy_uptime/lib/saved_objects/synthetics_monitor';
import { FtrProviderContext } from '../../ftr_provider_context';
import { getFixtureJson } from '../uptime/rest/helper/get_fixture_json';
import { comparePolicies, getTestSyntheticsPolicy } from './sample_data/test_policy';
import { PrivateLocationTestService } from './services/private_location_test_service';
export default function ({ getService }: FtrProviderContext) {
describe('PrivateLocationMonitor', function () {
describe('PrivateLocationAddMonitor', function () {
this.tags('skipCloud');
const kibanaServer = getService('kibanaServer');
const supertestAPI = getService('supertest');
@ -34,6 +35,7 @@ export default function ({ getService }: FtrProviderContext) {
const security = getService('security');
before(async () => {
await kibanaServer.savedObjects.clean({ types: [syntheticsMonitorType] });
await supertestAPI.post('/api/fleet/setup').set('kbn-xsrf', 'true').send().expect(200);
await supertestAPI
.post('/api/fleet/epm/packages/synthetics/0.12.0')
@ -85,6 +87,39 @@ export default function ({ getService }: FtrProviderContext) {
]);
});
it('does not add a monitor if there is an error in creating integration', async () => {
const newMonitor = { ...httpMonitorJson };
const invalidName = '[] - invalid name';
newMonitor.locations.push({
id: testFleetPolicyID,
label: 'Test private location 0',
isServiceManaged: false,
});
newMonitor.name = invalidName;
const apiResponse = await supertestAPI
.post(API_URLS.SYNTHETICS_MONITORS)
.set('kbn-xsrf', 'true')
.send(newMonitor)
.expect(500);
expect(apiResponse.body).eql({
statusCode: 500,
message:
'YAMLException: end of the stream or a document separator is expected at line 3, column 10:\n name: [] - invalid name\n ^',
error: 'Internal Server Error',
});
const apiGetResponse = await supertestAPI
.get(API_URLS.SYNTHETICS_MONITORS + `?query="${invalidName}"`)
.expect(200);
// verify that no monitor was added
expect(apiGetResponse.body.monitors?.length).eql(0);
});
let newMonitorId: string;
it('adds a monitor in private location', async () => {

View file

@ -0,0 +1,121 @@
/*
* 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 { v4 as uuidv4 } from 'uuid';
import expect from '@kbn/expect';
import { ProjectMonitorsRequest } 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 '../uptime/rest/helper/get_fixture_json';
import { PrivateLocationTestService } from './services/private_location_test_service';
import { SyntheticsMonitorTestService } from './services/synthetics_monitor_test_service';
export default function ({ getService }: FtrProviderContext) {
describe('AddProjectMonitorsPrivateLocations', function () {
this.tags('skipCloud');
const supertest = getService('supertest');
let projectMonitors: ProjectMonitorsRequest;
const monitorTestService = new SyntheticsMonitorTestService(getService);
let testPolicyId = '';
const testPrivateLocations = new PrivateLocationTestService(getService);
const setUniqueIds = (request: ProjectMonitorsRequest) => {
return {
...request,
monitors: request.monitors.map((monitor) => ({ ...monitor, id: uuidv4() })),
};
};
before(async () => {
await supertest.put(API_URLS.SYNTHETICS_ENABLEMENT).set('kbn-xsrf', 'true').expect(200);
await supertest.post('/api/fleet/setup').set('kbn-xsrf', 'true').send().expect(200);
await supertest
.post('/api/fleet/epm/packages/synthetics/0.12.0')
.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(() => {
projectMonitors = setUniqueIds({
monitors: getFixtureJson('project_browser_monitor').monitors,
});
});
it('project monitors - returns a failed monitor when creating integration fails', async () => {
const project = `test-project-${uuidv4()}`;
const secondMonitor = {
...projectMonitors.monitors[0],
id: 'test-id-2',
privateLocations: ['Test private location 0'],
};
const testMonitors = [
projectMonitors.monitors[0],
{ ...secondMonitor, name: '[] - invalid name' },
];
try {
const body = await monitorTestService.addProjectMonitors(project, testMonitors);
expect(body.createdMonitors.length).eql(1);
expect(body.failedMonitors[0].reason).eql(
'end of the stream or a document separator is expected at line 3, column 10:\n name: [] - invalid name\n ^'
);
} finally {
await Promise.all([
testMonitors.map((monitor) => {
return monitorTestService.deleteMonitorByJourney(projectMonitors, monitor.id, project);
}),
]);
}
});
it('project monitors - returns a failed monitor when editing integration fails', async () => {
const project = `test-project-${uuidv4()}`;
const secondMonitor = {
...projectMonitors.monitors[0],
id: 'test-id-2',
privateLocations: ['Test private location 0'],
};
const testMonitors = [projectMonitors.monitors[0], secondMonitor];
try {
const body = await monitorTestService.addProjectMonitors(project, testMonitors);
expect(body.createdMonitors.length).eql(2);
const editedBody = await monitorTestService.addProjectMonitors(project, testMonitors);
expect(editedBody.createdMonitors.length).eql(0);
expect(editedBody.updatedMonitors.length).eql(2);
testMonitors[1].name = '[] - invalid name';
const editedBodyError = await monitorTestService.addProjectMonitors(project, testMonitors);
expect(editedBodyError.createdMonitors.length).eql(0);
expect(editedBodyError.updatedMonitors.length).eql(1);
expect(editedBodyError.failedMonitors.length).eql(1);
expect(editedBodyError.failedMonitors[0].details).eql(
'Failed to update journey: test-id-2'
);
expect(editedBodyError.failedMonitors[0].reason).eql(
'end of the stream or a document separator is expected at line 3, column 10:\n name: [] - invalid name\n ^'
);
} finally {
await Promise.all([
testMonitors.map((monitor) => {
return monitorTestService.deleteMonitorByJourney(projectMonitors, monitor.id, project);
}),
]);
}
});
});
}

View file

@ -29,5 +29,6 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) {
loadTestFile(require.resolve('./synthetics_enablement'));
loadTestFile(require.resolve('./sync_global_params'));
loadTestFile(require.resolve('./add_edit_params'));
loadTestFile(require.resolve('./add_monitor_project_private_location'));
});
}

View file

@ -0,0 +1,56 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { API_URLS } from '@kbn/synthetics-plugin/common/constants';
import { syntheticsMonitorType } from '@kbn/synthetics-plugin/server/legacy_uptime/lib/saved_objects/synthetics_monitor';
import { FtrProviderContext } from '../../../ftr_provider_context';
import { KibanaSupertestProvider } from '../../../../../../test/api_integration/services/supertest';
export class SyntheticsMonitorTestService {
private supertest: ReturnType<typeof KibanaSupertestProvider>;
constructor(getService: FtrProviderContext['getService']) {
this.supertest = getService('supertest');
}
async addProjectMonitors(project: string, monitors: any) {
const { body } = await this.supertest
.put(API_URLS.SYNTHETICS_MONITORS_PROJECT_UPDATE.replace('{projectName}', project))
.set('kbn-xsrf', 'true')
.send({ monitors })
.expect(200);
return body;
}
async deleteMonitorByJourney(
projectMonitors: any,
journeyId: string,
projectId: string,
space: string = 'default'
) {
try {
const response = await this.supertest
.get(`/s/${space}${API_URLS.SYNTHETICS_MONITORS}`)
.query({
filter: `${syntheticsMonitorType}.attributes.journey_id: "${journeyId}" AND ${syntheticsMonitorType}.attributes.project_id: "${projectId}"`,
})
.set('kbn-xsrf', 'true')
.expect(200);
const { monitors } = response.body;
if (monitors[0]?.id) {
await this.supertest
.delete(`/s/${space}${API_URLS.SYNTHETICS_MONITORS}/${monitors[0].id}`)
.set('kbn-xsrf', 'true')
.send(projectMonitors)
.expect(200);
}
} catch (e) {
// eslint-disable-next-line no-console
console.error(e);
}
}
}