[Synthetics] Global params Public APIs (#169669)

This commit is contained in:
Shahzad 2023-10-27 16:16:40 +02:00 committed by GitHub
parent 54385c577d
commit 8bbb58f19a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 903 additions and 309 deletions

View file

@ -0,0 +1,123 @@
[[add-parameters-api]]
== Add Parameters API
++++
<titleabbrev>Add Parameters</titleabbrev>
++++
Adds one or more parameters to the synthetics app.
=== {api-request-title}
`POST <kibana host>:<port>/api/synthetics/params`
`POST <kibana host>:<port>/s/<space_id>/api/synthetics/params`
=== {api-prereq-title}
You must have `all` privileges for the *Synthetics* feature in the *{observability}* section of the
<<kibana-feature-privileges,{kib} feature privileges>>.
[[parameters-add-request-body]]
==== Request body
The request body can contain either a single parameter object or an array of parameter objects. The parameter object schema includes the following attributes:
`key`::
(Required, string) The key of the parameter.
`value`::
(Required, string) The value associated with the parameter.
`description`::
(Optional, string) A description of the parameter.
`tags`::
(Optional, array of strings) An array of tags to categorize the parameter.
`share_across_spaces`::
(Optional, boolean) Whether the parameter should be shared across spaces.
When adding a single parameter, provide a single object. When adding multiple parameters, provide an array of parameter objects.
[[parameters-add-example]]
==== Example
Here are examples of POST requests to add parameters, either as a single parameter or as an array of parameters:
To add a single parameter:
[source,sh]
--------------------------------------------------
POST /api/synthetics/params
{
"key": "your-key-name",
"value": "your-parameter-value",
"description": "Param to use in browser monitor",
"tags": ["authentication", "security"],
"share_across_spaces": true
}
--------------------------------------------------
To add multiple parameters:
[source,sh]
--------------------------------------------------
POST /api/synthetics/params
[
{
"key": "param1",
"value": "value1"
},
{
"key": "param2",
"value": "value2"
}
]
--------------------------------------------------
The API returns a response based on the request. If you added a single parameter, it will return a single parameter object. If you added multiple parameters, it will return an array of parameter objects.
[[parameters-add-response-example]]
==== Response Example
The API response includes the created parameter(s) as JSON objects, where each parameter object has the following attributes:
- `id` (string): The unique identifier of the parameter.
- `key` (string): The key of the parameter.
- `value` (string): The value associated with the parameter.
- `description` (string, optional): The description of the parameter.
- `tags` (array of strings, optional): An array of tags associated with the parameter.
- `share_across_spaces` (boolean, optional): Indicates whether the parameter is shared across spaces.
Here's an example response for a single added parameter:
[source,json]
--------------------------------------------------
{
"id": "unique-parameter-id",
"key": "your-key-name",
"value": "your-param-value",
"description": "Param to use in browser monitor",
"tags": ["authentication", "security"],
"share_across_spaces": true
}
--------------------------------------------------
And here's an example response for adding multiple parameters:
[source,json]
--------------------------------------------------
[
{
"id": "param1-id",
"key": "param1",
"value": "value1"
},
{
"id": "param2-id",
"key": "param2",
"value": "value2"
}
]
--------------------------------------------------

View file

@ -0,0 +1,67 @@
[[delete-parameters-api]]
== Delete Parameters API
++++
<titleabbrev>Delete Parameters</titleabbrev>
++++
Deletes one or more parameters from the Synthetics app.
=== {api-request-title}
`DELETE <kibana host>:<port>/api/synthetics/params`
`DELETE <kibana host>:<port>/s/<space_id>/api/synthetics/params`
=== {api-prereq-title}
You must have `all` privileges for the *Synthetics* feature in the *{observability}* section of the
<<kibana-feature-privileges,{kib} feature privileges>>.
You must have `all` privileges for the *Synthetics* feature in the *{observability}* section of the
<<kibana-feature-privileges,{kib} feature privileges>>.
[[parameters-delete-request-body]]
==== Request Body
The request body should contain an array of parameter IDs that you want to delete.
`ids`::
(Required, array of strings) An array of parameter IDs to delete.
Here is an example of a DELETE request to delete a list of parameters by ID:
[source,sh]
--------------------------------------------------
DELETE /api/synthetics/params
{
"ids": [
"param1-id",
"param2-id"
]
}
--------------------------------------------------
[[parameters-delete-response-example]]
==== Response Example
The API response includes information about the deleted parameters, where each entry in the response array contains the following attributes:
- `id` (string): The unique identifier of the deleted parameter.
- `deleted` (boolean): Indicates whether the parameter was successfully deleted (`true` if deleted, `false` if not).
Here's an example response for deleting multiple parameters:
[source,sh]
--------------------------------------------------
[
{
"id": "param1-id",
"deleted": true
},
{
"id": "param2-id",
"deleted": true
}
]
--------------------------------------------------

View file

@ -0,0 +1,70 @@
[[edit-parameter-by-id-api]]
== Edit Parameter by ID API
++++
<titleabbrev>Edit Parameter</titleabbrev>
++++
Edits a parameter with the specified ID.
=== {api-request-title}
`PUT <kibana host>:<port>/api/synthetics/params`
`PUT <kibana host>:<port>/s/<space_id>/api/synthetics/params`
=== {api-prereq-title}
You must have `all` privileges for the *Synthetics* feature in the *{observability}* section of the
<<kibana-feature-privileges,{kib} feature privileges>>.
[[parameter-edit-path-params]]
==== Path Parameters
`id`::
(Required, string) The unique identifier of the parameter to be edited.
[[parameter-edit-request-body]]
==== Request body
The request body should contain the following attributes:
`key`::
(Required, string) The key of the parameter.
`value`::
(Required, string) The updated value associated with the parameter.
`description`::
(Optional, string) The updated description of the parameter.
`tags`::
(Optional, array of strings) An array of updated tags to categorize the parameter.
[[parameter-edit-example]]
==== Example
Here is an example of a PUT request to edit a parameter by its ID:
[source,sh]
--------------------------------------------------
PUT /api/synthetics/params/param_id1
{
"key": "updated_param_key",
"value": "updated-param-value",
"description": "Updated Param to be used in browser monitor",
"tags": ["authentication", "security", "updated"]
}
--------------------------------------------------
The API returns the updated parameter as follows:
[source,json]
--------------------------------------------------
{
"id": "param_id1",
"key": "updated_param_key",
"value": "updated-param-value",
"description": "Updated Param to be used in browser monitor",
"tags": ["authentication", "security", "updated"]
}
--------------------------------------------------

View file

@ -0,0 +1,128 @@
[[get-parameters-api]]
== Get Parameters API
++++
<titleabbrev>Get Parameters</titleabbrev>
++++
Retrieves parameters based on the provided criteria.
=== {api-request-title}
`GET <kibana host>:<port>/api/synthetics/params/{id?}`
`GET <kibana host>:<port>/s/<space_id>/api/synthetics/params/{id?}`
=== {api-prereq-title}
You must have `read` privileges for the *Synthetics* feature in the *{observability}* section of the
<<kibana-feature-privileges,{kib} feature privileges>>.
[[parameters-get-query-params]]
==== Query Parameters
`id`::
(Optional, string) The unique identifier of the parameter. If provided, this API will retrieve a specific parameter by its ID. If not provided, it will retrieve a list of all parameters.
[[parameters-get-response-example]]
==== Response Example
The API response includes parameter(s) as JSON objects, where each parameter object has the following attributes:
- `id` (string): The unique identifier of the parameter.
- `key` (string): The key of the parameter.
If the user has read-only permissions to the Synthetics app, the following additional attributes will be included:
- `description` (string, optional): The description of the parameter.
- `tags` (array of strings, optional): An array of tags associated with the parameter.
- `namespaces` (array of strings): Namespaces associated with the parameter.
If the user has write permissions, the following additional attribute will be included:
- `value` (string): The value associated with the parameter.
Here's an example request for retrieving a single parameter by its ID:
[source,sh]
--------------------------------------------------
GET /api/synthetics/params/unique-parameter-id
--------------------------------------------------
Here's an example response for retrieving a single parameter by its ID:
For users with read-only permissions:
[source,json]
--------------------------------------------------
{
"id": "unique-parameter-id",
"key": "your-api-key",
"description": "Param to use in browser monitor",
"tags": ["authentication", "security"],
"namespaces": ["namespace1", "namespace2"]
}
--------------------------------------------------
For users with write permissions:
[source,json]
--------------------------------------------------
{
"id": "unique-parameter-id",
"key": "your-param-key",
"description": "Param to use in browser monitor",
"tags": ["authentication", "security"],
"namespaces": ["namespace1", "namespace2"],
"value": "your-param-value"
}
--------------------------------------------------
And here's an example response for retrieving a list of parameters:
For users with read-only permissions:
[source,json]
--------------------------------------------------
[
{
"id": "param1-id",
"key": "param1",
"description": "Description for param1",
"tags": ["tag1", "tag2"],
"namespaces": ["namespace1"]
},
{
"id": "param2-id",
"key": "param2",
"description": "Description for param2",
"tags": ["tag3"],
"namespaces": ["namespace2"]
}
]
--------------------------------------------------
For users with write permissions:
[source,json]
--------------------------------------------------
[
{
"id": "param1-id",
"key": "param1",
"description": "Description for param1",
"tags": ["tag1", "tag2"],
"namespaces": ["namespace1"],
"value": "value1"
},
{
"id": "param2-id",
"key": "param2",
"description": "Description for param2",
"tags": ["tag3"],
"namespaces": ["namespace2"],
"value": "value2"
}
]
--------------------------------------------------

View file

@ -0,0 +1,18 @@
[[synthetics-apis]]
== Synthetics APIs
The following APIs are available for Synthetics.
* <<get-parameters-api, Get Parameters API>> to get a parameter(s).
* <<add-parameters-api, Create Parameter API>> to create a parameter.
* <<edit-parameter-by-id-api, Edit Parameter API>> to edit a parameter.
* <<delete-parameters-api, Delete Parameter API>> to delete a parameter.
include::params/add-param.asciidoc[leveloffset=+1]
include::params/get-params.asciidoc[leveloffset=+1]
include::params/edit-param.asciidoc[leveloffset=+1]
include::params/delete-param.asciidoc[leveloffset=+1]

View file

@ -109,4 +109,5 @@ include::{kib-repo-dir}/api/osquery-manager.asciidoc[]
include::{kib-repo-dir}/api/short-urls.asciidoc[]
include::{kib-repo-dir}/api/task-manager/health.asciidoc[]
include::{kib-repo-dir}/api/upgrade-assistant.asciidoc[]
include::{kib-repo-dir}/api/synthetics/synthetics-api.asciidoc[]
include::{kib-repo-dir}/api/uptime-api.asciidoc[]

View file

@ -6,6 +6,9 @@
*/
export enum SYNTHETICS_API_URLS {
// public apis
PARAMS = `/api/synthetics/params`,
// Service end points
INDEX_TEMPLATES = '/internal/synthetics/service/index_templates',
SERVICE_LOCATIONS = '/internal/uptime/service/locations',
@ -24,7 +27,6 @@ export enum SYNTHETICS_API_URLS {
PING_STATUSES = '/internal/synthetics/ping_statuses',
OVERVIEW_STATUS = `/internal/synthetics/overview_status`,
INDEX_SIZE = `/internal/synthetics/index_size`,
PARAMS = `/internal/synthetics/params`,
AGENT_POLICIES = `/internal/synthetics/agent_policies`,
PRIVATE_LOCATIONS = `/internal/synthetics/private_locations`,
PRIVATE_LOCATIONS_MONITORS = `/internal/synthetics/private_locations/monitors`,

View file

@ -73,3 +73,5 @@ export const SYNTHETICS_INDEX_PATTERN = 'synthetics-*';
export const LICENSE_NOT_ACTIVE_ERROR = 'License not active';
export const LICENSE_MISSING_ERROR = 'Missing license information';
export const LICENSE_NOT_SUPPORTED_ERROR = 'License not supported';
export const INITIAL_REST_VERSION = '2023-10-31';

View file

@ -108,8 +108,9 @@ export const AddParamFlyout = ({
useEffect(() => {
if (isEditingItem) {
const { id: _id, ...dataToEdit } = isEditingItem;
setIsFlyoutVisible(true);
form.reset(isEditingItem);
form.reset(dataToEdit);
}
// no need to add form value, it keeps changing on reset
// eslint-disable-next-line react-hooks/exhaustive-deps

View file

@ -55,7 +55,7 @@ export const DeleteParam = ({
<p data-test-subj="uptimeDeleteParamFailure">
{' '}
{i18n.translate('xpack.synthetics.paramManagement.paramDeleteFailuresMessage.name', {
defaultMessage: 'Param {name} deleted successfully.',
defaultMessage: 'Param {name} failed to delete.',
values: { name },
})}
</p>

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { SYNTHETICS_API_URLS } from '../../../../../common/constants';
import { INITIAL_REST_VERSION, SYNTHETICS_API_URLS } from '../../../../../common/constants';
import {
DeleteParamsResponse,
SyntheticsParamRequest,
@ -18,7 +18,7 @@ import { apiService } from '../../../../utils/api_service/api_service';
export const getGlobalParams = async (): Promise<SyntheticsParams[]> => {
return apiService.get<SyntheticsParams[]>(
SYNTHETICS_API_URLS.PARAMS,
undefined,
{ version: INITIAL_REST_VERSION },
SyntheticsParamsReadonlyCodec
);
};
@ -26,7 +26,9 @@ export const getGlobalParams = async (): Promise<SyntheticsParams[]> => {
export const addGlobalParam = async (
paramRequest: SyntheticsParamRequest
): Promise<SyntheticsParams> =>
apiService.post(SYNTHETICS_API_URLS.PARAMS, paramRequest, SyntheticsParamsCodec);
apiService.post(SYNTHETICS_API_URLS.PARAMS, paramRequest, SyntheticsParamsCodec, {
version: INITIAL_REST_VERSION,
});
export const editGlobalParam = async ({
paramRequest,
@ -36,15 +38,15 @@ export const editGlobalParam = async ({
paramRequest: SyntheticsParamRequest;
}): Promise<SyntheticsParams> =>
apiService.put<SyntheticsParams>(
SYNTHETICS_API_URLS.PARAMS,
SYNTHETICS_API_URLS.PARAMS + `/${id}`,
paramRequest,
SyntheticsParamsCodec,
{
id,
...paramRequest,
},
SyntheticsParamsCodec
version: INITIAL_REST_VERSION,
}
);
export const deleteGlobalParams = async (ids: string[]): Promise<DeleteParamsResponse[]> =>
apiService.delete(SYNTHETICS_API_URLS.PARAMS, {
ids: JSON.stringify(ids),
apiService.delete(SYNTHETICS_API_URLS.PARAMS, undefined, {
ids,
});

View file

@ -101,8 +101,15 @@ class ApiService {
return this.parseResponse(response, apiUrl, decodeType);
}
public async delete<T>(apiUrl: string, params?: HttpFetchQuery) {
const response = await this._http!.delete<T>({ path: apiUrl, query: params });
public async delete<T>(apiUrl: string, params: Params = {}, data?: any) {
const { version, ...queryParams } = params;
const response = await this._http!.delete<T>({
path: apiUrl,
query: queryParams,
body: JSON.stringify(data),
version,
});
if (response instanceof Error) {
throw response;

View file

@ -5,6 +5,8 @@
* 2.0.
*/
import { getSyntheticsParamsRoute } from './settings/params/params';
import { editSyntheticsParamsRoute } from './settings/params/edit_param';
import { getConnectorTypesRoute } from './default_alerts/get_connector_types';
import { getActionConnectorsRoute } from './default_alerts/get_action_connectors';
import { SyntheticsRestApiRouteFactory } from './types';
@ -18,8 +20,6 @@ import { createLastSuccessfulCheckRoute } from './pings/last_successful_check';
import { createJourneyFailedStepsRoute, createJourneyRoute } from './pings/journeys';
import { updateDefaultAlertingRoute } from './default_alerts/update_default_alert';
import { syncParamsSyntheticsParamsRoute } from './settings/sync_global_params';
import { editSyntheticsParamsRoute } from './settings/edit_param';
import { getSyntheticsParamsRoute } from './settings/params';
import { getIndexSizesRoute } from './settings/settings';
import { getAPIKeySyntheticsRoute } from './monitor_cruds/get_api_key';
import { getServiceLocationsRoute } from './synthetics_service/get_service_locations';
@ -44,8 +44,6 @@ import { addSyntheticsProjectMonitorRoute } from './monitor_cruds/add_monitor_pr
import { syntheticsGetPingsRoute, syntheticsGetPingStatusesRoute } from './pings';
import { createGetCurrentStatusRoute } from './overview_status/overview_status';
import { getHasIntegrationMonitorsRoute } from './fleet/get_has_integration_monitors';
import { addSyntheticsParamsRoute } from './settings/add_param';
import { deleteSyntheticsParamsRoute } from './settings/delete_param';
import { enableDefaultAlertingRoute } from './default_alerts/enable_default_alert';
import { getDefaultAlertingRoute } from './default_alerts/get_default_alert';
import { createNetworkEventsRoute } from './network_events';
@ -55,6 +53,8 @@ import { getPrivateLocationsRoute } from './settings/private_locations/get_priva
import { getSyntheticsFilters } from './filters/filters';
import { getAllSyntheticsMonitorRoute } from './monitor_cruds/get_monitors_list';
import { getLocationMonitors } from './settings/private_locations/get_location_monitors';
import { addSyntheticsParamsRoute } from './settings/params/add_param';
import { deleteSyntheticsParamsRoute } from './settings/params/delete_param';
export const syntheticsAppRestApiRoutes: SyntheticsRestApiRouteFactory[] = [
addSyntheticsMonitorRoute,
@ -79,10 +79,6 @@ export const syntheticsAppRestApiRoutes: SyntheticsRestApiRouteFactory[] = [
getHasIntegrationMonitorsRoute,
createGetCurrentStatusRoute,
getIndexSizesRoute,
getSyntheticsParamsRoute,
editSyntheticsParamsRoute,
addSyntheticsParamsRoute,
deleteSyntheticsParamsRoute,
syncParamsSyntheticsParamsRoute,
enableDefaultAlertingRoute,
getDefaultAlertingRoute,
@ -105,3 +101,10 @@ export const syntheticsAppRestApiRoutes: SyntheticsRestApiRouteFactory[] = [
getActionConnectorsRoute,
getConnectorTypesRoute,
];
export const syntheticsAppPublicRestApiRoutes: SyntheticsRestApiRouteFactory[] = [
getSyntheticsParamsRoute,
editSyntheticsParamsRoute,
addSyntheticsParamsRoute,
deleteSyntheticsParamsRoute,
];

View file

@ -1,79 +0,0 @@
/*
* 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 { schema } from '@kbn/config-schema';
import { ALL_SPACES_ID } from '@kbn/security-plugin/common/constants';
import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common';
import { IKibanaResponse } from '@kbn/core/server';
import { SyntheticsRestApiRouteFactory } from '../types';
import {
SyntheticsParamRequest,
SyntheticsParams,
SyntheticsParamSOAttributes,
} from '../../../common/runtime_types';
import { syntheticsParamType } from '../../../common/types/saved_objects';
import { SYNTHETICS_API_URLS } from '../../../common/constants';
export const addSyntheticsParamsRoute: SyntheticsRestApiRouteFactory = () => ({
method: 'POST',
path: SYNTHETICS_API_URLS.PARAMS,
validate: {
body: schema.object({
key: schema.string(),
value: schema.string(),
description: schema.maybe(schema.string()),
tags: schema.maybe(schema.arrayOf(schema.string())),
share_across_spaces: schema.maybe(schema.boolean()),
}),
},
writeAccess: true,
handler: async ({
request,
response,
server,
savedObjectsClient,
}): Promise<IKibanaResponse<SyntheticsParams>> => {
try {
const { id: spaceId } = (await server.spaces?.spacesService.getActiveSpace(request)) ?? {
id: DEFAULT_SPACE_ID,
};
const { share_across_spaces: shareAcrossSpaces, ...data } =
request.body as SyntheticsParamRequest;
const {
attributes: { key, tags, description },
id,
namespaces,
} = await savedObjectsClient.create<Omit<SyntheticsParamSOAttributes, 'id'>>(
syntheticsParamType,
data,
{
initialNamespaces: shareAcrossSpaces ? [ALL_SPACES_ID] : [spaceId],
}
);
return response.ok({
body: {
id,
description,
key,
namespaces,
tags,
value: data.value,
},
});
} catch (error) {
if (error.output?.statusCode === 404) {
const spaceId = server.spaces?.spacesService.getSpaceId(request) ?? DEFAULT_SPACE_ID;
return response.notFound({
body: { message: `Kibana space '${spaceId}' does not exist` },
});
}
throw error;
}
},
});

View file

@ -1,40 +0,0 @@
/*
* 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 { IKibanaResponse } from '@kbn/core/server';
import { schema } from '@kbn/config-schema';
import { SyntheticsRestApiRouteFactory } from '../types';
import { syntheticsParamType } from '../../../common/types/saved_objects';
import { SYNTHETICS_API_URLS } from '../../../common/constants';
import { DeleteParamsResponse } from '../../../common/runtime_types';
export const deleteSyntheticsParamsRoute: SyntheticsRestApiRouteFactory = () => ({
method: 'DELETE',
path: SYNTHETICS_API_URLS.PARAMS,
validate: {
query: schema.object({
ids: schema.string(),
}),
},
writeAccess: true,
handler: async ({
savedObjectsClient,
request,
response,
}): Promise<IKibanaResponse<DeleteParamsResponse[]>> => {
const { ids } = request.query as { ids: string };
const parsedIds = JSON.parse(ids) as string[];
const result = await savedObjectsClient.bulkDelete(
parsedIds.map((id) => ({ type: syntheticsParamType, id })),
{ force: true }
);
return response.ok({
body: result.statuses.map(({ id, success }) => ({ id, deleted: success })),
});
},
});

View file

@ -1,69 +0,0 @@
/*
* 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 { schema } from '@kbn/config-schema';
import { IKibanaResponse, SavedObject } from '@kbn/core/server';
import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common';
import { SyntheticsRestApiRouteFactory } from '../types';
import { SyntheticsParamRequest, SyntheticsParams } from '../../../common/runtime_types';
import { syntheticsParamType } from '../../../common/types/saved_objects';
import { SYNTHETICS_API_URLS } from '../../../common/constants';
export const editSyntheticsParamsRoute: SyntheticsRestApiRouteFactory = () => ({
method: 'PUT',
path: SYNTHETICS_API_URLS.PARAMS,
validate: {
body: schema.object({
id: schema.string(),
key: schema.string(),
value: schema.string(),
description: schema.maybe(schema.string()),
tags: schema.maybe(schema.arrayOf(schema.string())),
share_across_spaces: schema.maybe(schema.boolean()),
}),
},
writeAccess: true,
handler: async ({
savedObjectsClient,
request,
response,
server,
}): Promise<IKibanaResponse<SyntheticsParams>> => {
try {
const { id: _spaceId } = (await server.spaces?.spacesService.getActiveSpace(request)) ?? {
id: DEFAULT_SPACE_ID,
};
const {
share_across_spaces: shareAcrossSpaces,
id,
...data
} = request.body as SyntheticsParamRequest & {
id: string;
};
const { value } = data;
const {
id: responseId,
attributes: { key, tags, description },
namespaces,
} = (await savedObjectsClient.update(
syntheticsParamType,
id,
data
)) as SavedObject<SyntheticsParams>;
return response.ok({ body: { id: responseId, key, tags, description, namespaces, value } });
} catch (error) {
if (error.output?.statusCode === 404) {
const spaceId = server.spaces?.spacesService.getSpaceId(request) ?? DEFAULT_SPACE_ID;
return response.notFound({ body: { message: `Kibana space '${spaceId}' does not exist` } });
}
throw error;
}
},
});

View file

@ -1,74 +0,0 @@
/*
* 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 { IKibanaResponse } from '@kbn/core/server';
import { SavedObjectsFindResult } from '@kbn/core-saved-objects-api-server';
import { SyntheticsRestApiRouteFactory } from '../types';
import { syntheticsParamType } from '../../../common/types/saved_objects';
import { SYNTHETICS_API_URLS } from '../../../common/constants';
import { SyntheticsParams, SyntheticsParamsReadonly } from '../../../common/runtime_types';
type SyntheticsParamsResponse =
| IKibanaResponse<SyntheticsParams[]>
| IKibanaResponse<SyntheticsParamsReadonly[]>;
export const getSyntheticsParamsRoute: SyntheticsRestApiRouteFactory<
SyntheticsParamsResponse
> = () => ({
method: 'GET',
path: SYNTHETICS_API_URLS.PARAMS,
validate: {},
handler: async ({ savedObjectsClient, request, response, server, spaceId }) => {
try {
const encryptedSavedObjectsClient = server.encryptedSavedObjects.getClient();
const canSave =
(await server.coreStart?.capabilities.resolveCapabilities(request)).uptime.save ?? false;
if (canSave) {
const finder =
await encryptedSavedObjectsClient.createPointInTimeFinderDecryptedAsInternalUser<SyntheticsParams>(
{
type: syntheticsParamType,
perPage: 1000,
namespaces: [spaceId],
}
);
const hits: Array<SavedObjectsFindResult<SyntheticsParams>> = [];
for await (const result of finder.find()) {
hits.push(...result.saved_objects);
}
return response.ok({
body: hits.map(({ id, attributes, namespaces }) => ({
...attributes,
id,
namespaces,
})),
});
} else {
const data = await savedObjectsClient.find<SyntheticsParamsReadonly>({
type: syntheticsParamType,
perPage: 10000,
});
return response.ok({
body: data.saved_objects.map(({ id, attributes, namespaces }) => ({
...attributes,
namespaces,
id,
})),
});
}
} catch (error) {
if (error.output?.statusCode === 404) {
return response.notFound({ body: { message: `Kibana space '${spaceId}' does not exist` } });
}
throw error;
}
},
});

View file

@ -0,0 +1,113 @@
/*
* 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 { schema } from '@kbn/config-schema';
import { ALL_SPACES_ID } from '@kbn/security-plugin/common/constants';
import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common';
import { SavedObject, SavedObjectsBulkCreateObject } from '@kbn/core-saved-objects-api-server';
import { SyntheticsRestApiRouteFactory } from '../../types';
import {
SyntheticsParamRequest,
SyntheticsParams,
SyntheticsParamSOAttributes,
} from '../../../../common/runtime_types';
import { syntheticsParamType } from '../../../../common/types/saved_objects';
import { SYNTHETICS_API_URLS } from '../../../../common/constants';
const ParamsObjectSchema = schema.object({
key: schema.string(),
value: schema.string(),
description: schema.maybe(schema.string()),
tags: schema.maybe(schema.arrayOf(schema.string())),
share_across_spaces: schema.maybe(schema.boolean()),
});
export const addSyntheticsParamsRoute: SyntheticsRestApiRouteFactory<
SyntheticsParams | SyntheticsParams[]
> = () => ({
method: 'POST',
path: SYNTHETICS_API_URLS.PARAMS,
validate: {},
validation: {
request: {
body: schema.oneOf([ParamsObjectSchema, schema.arrayOf(ParamsObjectSchema)]),
},
},
writeAccess: true,
handler: async ({ request, response, server, savedObjectsClient }) => {
try {
const { id: spaceId } = (await server.spaces?.spacesService.getActiveSpace(request)) ?? {
id: DEFAULT_SPACE_ID,
};
const savedObjectsData = parseParamBody(
spaceId,
request.body as SyntheticsParamRequest[] | SyntheticsParamRequest
);
const result = await savedObjectsClient.bulkCreate<Omit<SyntheticsParamSOAttributes, 'id'>>(
savedObjectsData
);
if (savedObjectsData.length > 1) {
return result.saved_objects.map((savedObject) => {
return toClientResponse(savedObject);
});
} else {
return toClientResponse(result.saved_objects[0]);
}
} catch (error) {
if (error.output?.statusCode === 404) {
const spaceId = server.spaces?.spacesService.getSpaceId(request) ?? DEFAULT_SPACE_ID;
return response.notFound({
body: { message: `Kibana space '${spaceId}' does not exist` },
});
}
throw error;
}
},
});
const toClientResponse = (savedObject: SavedObject<Omit<SyntheticsParamSOAttributes, 'id'>>) => {
const { id, attributes: data, namespaces } = savedObject;
const { description, key, tags } = data;
return {
id,
description,
key,
namespaces,
tags,
value: data.value,
};
};
const parseParamBody = (
spaceId: string,
body: SyntheticsParamRequest[] | SyntheticsParamRequest
): Array<SavedObjectsBulkCreateObject<Omit<SyntheticsParamSOAttributes, 'id'>>> => {
if (Array.isArray(body)) {
const params = body as SyntheticsParamRequest[];
return params.map((param) => {
const { share_across_spaces: shareAcrossSpaces, ...data } = param;
return {
type: syntheticsParamType,
attributes: data,
initialNamespaces: shareAcrossSpaces ? [ALL_SPACES_ID] : [spaceId],
};
});
}
const { share_across_spaces: shareAcrossSpaces, ...data } = body;
return [
{
type: syntheticsParamType,
attributes: data,
initialNamespaces: shareAcrossSpaces ? [ALL_SPACES_ID] : [spaceId],
},
];
};

View file

@ -0,0 +1,40 @@
/*
* 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 { schema } from '@kbn/config-schema';
import { SyntheticsRestApiRouteFactory } from '../../types';
import { syntheticsParamType } from '../../../../common/types/saved_objects';
import { SYNTHETICS_API_URLS } from '../../../../common/constants';
import { DeleteParamsResponse } from '../../../../common/runtime_types';
export const deleteSyntheticsParamsRoute: SyntheticsRestApiRouteFactory<
DeleteParamsResponse[],
unknown,
unknown,
{ ids: string[] }
> = () => ({
method: 'DELETE',
path: SYNTHETICS_API_URLS.PARAMS,
validate: {},
validation: {
request: {
body: schema.object({
ids: schema.arrayOf(schema.string()),
}),
},
},
writeAccess: true,
handler: async ({ savedObjectsClient, request }) => {
const { ids } = request.body;
const result = await savedObjectsClient.bulkDelete(
ids.map((id) => ({ type: syntheticsParamType, id })),
{ force: true }
);
return result.statuses.map(({ id, success }) => ({ id, deleted: success }));
},
});

View file

@ -0,0 +1,76 @@
/*
* 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 { schema, TypeOf } from '@kbn/config-schema';
import { SavedObject } from '@kbn/core/server';
import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common';
import { SyntheticsRestApiRouteFactory } from '../../types';
import { SyntheticsParamRequest, SyntheticsParams } from '../../../../common/runtime_types';
import { syntheticsParamType } from '../../../../common/types/saved_objects';
import { SYNTHETICS_API_URLS } from '../../../../common/constants';
const RequestParamsSchema = schema.object({
id: schema.string(),
});
type RequestParams = TypeOf<typeof RequestParamsSchema>;
export const editSyntheticsParamsRoute: SyntheticsRestApiRouteFactory<
SyntheticsParams,
RequestParams
> = () => ({
method: 'PUT',
path: SYNTHETICS_API_URLS.PARAMS + '/{id}',
validate: {},
validation: {
request: {
params: RequestParamsSchema,
body: schema.object({
key: schema.string(),
value: schema.string(),
description: schema.maybe(schema.string()),
tags: schema.maybe(schema.arrayOf(schema.string())),
share_across_spaces: schema.maybe(schema.boolean()),
}),
},
},
writeAccess: true,
handler: async ({ savedObjectsClient, request, server, response }) => {
try {
const { id: _spaceId } = (await server.spaces?.spacesService.getActiveSpace(request)) ?? {
id: DEFAULT_SPACE_ID,
};
const { id } = request.params;
const { share_across_spaces: _shareAcrossSpaces, ...data } =
request.body as SyntheticsParamRequest & {
id: string;
};
const { value } = data;
const {
id: responseId,
attributes: { key, tags, description },
namespaces,
} = (await savedObjectsClient.update(
syntheticsParamType,
id,
data
)) as SavedObject<SyntheticsParams>;
return { id: responseId, key, tags, description, namespaces, value };
} catch (error) {
if (error.output?.statusCode === 404) {
const spaceId = server.spaces?.spacesService.getSpaceId(request) ?? DEFAULT_SPACE_ID;
return response.notFound({
body: { message: `Kibana space '${spaceId}' does not exist` },
});
}
throw error;
}
},
});

View file

@ -0,0 +1,102 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { SavedObject, SavedObjectsFindResult } from '@kbn/core-saved-objects-api-server';
import { schema, TypeOf } from '@kbn/config-schema';
import { SyntheticsRestApiRouteFactory } from '../../types';
import { syntheticsParamType } from '../../../../common/types/saved_objects';
import { SYNTHETICS_API_URLS } from '../../../../common/constants';
import { SyntheticsParams, SyntheticsParamsReadonly } from '../../../../common/runtime_types';
const RequestParamsSchema = schema.object({
id: schema.maybe(schema.string()),
});
type RequestParams = TypeOf<typeof RequestParamsSchema>;
export const getSyntheticsParamsRoute: SyntheticsRestApiRouteFactory<
SyntheticsParams[] | SyntheticsParamsReadonly[] | SyntheticsParams | SyntheticsParamsReadonly,
RequestParams
> = () => ({
method: 'GET',
path: SYNTHETICS_API_URLS.PARAMS + '/{id?}',
validate: {},
validation: {
request: {
params: RequestParamsSchema,
},
},
handler: async ({ savedObjectsClient, request, response, server, spaceId }) => {
try {
const { id: paramId } = request.params;
const encryptedSavedObjectsClient = server.encryptedSavedObjects.getClient();
const canSave =
(await server.coreStart?.capabilities.resolveCapabilities(request)).uptime.save ?? false;
if (canSave) {
if (paramId) {
const savedObject =
await encryptedSavedObjectsClient.getDecryptedAsInternalUser<SyntheticsParams>(
syntheticsParamType,
paramId,
{ namespace: spaceId }
);
return toClientResponse(savedObject);
}
const finder =
await encryptedSavedObjectsClient.createPointInTimeFinderDecryptedAsInternalUser<SyntheticsParams>(
{
type: syntheticsParamType,
perPage: 1000,
namespaces: [spaceId],
}
);
const hits: Array<SavedObjectsFindResult<SyntheticsParams>> = [];
for await (const result of finder.find()) {
hits.push(...result.saved_objects);
}
return hits.map((savedObject) => toClientResponse(savedObject));
} else {
if (paramId) {
const savedObject = await savedObjectsClient.get<SyntheticsParamsReadonly>(
syntheticsParamType,
paramId
);
return toClientResponse(savedObject);
}
const data = await savedObjectsClient.find<SyntheticsParamsReadonly>({
type: syntheticsParamType,
perPage: 10000,
});
return data.saved_objects.map((savedObject) => toClientResponse(savedObject));
}
} catch (error) {
if (error.output?.statusCode === 404) {
return response.notFound({ body: { message: `Kibana space '${spaceId}' does not exist` } });
}
throw error;
}
},
});
const toClientResponse = (
savedObject: SavedObject<SyntheticsParams | SyntheticsParamsReadonly>
) => {
const { id, attributes, namespaces } = savedObject;
return {
...attributes,
id,
namespaces,
};
};

View file

@ -16,6 +16,7 @@ import {
KibanaResponseFactory,
IKibanaResponse,
} from '@kbn/core/server';
import { FullValidationConfig } from '@kbn/core-http-server';
import { UptimeEsClient } from '../lib';
import { SyntheticsServerSetup, UptimeRequestHandlerContext } from '../types';
import { SyntheticsMonitorClient } from '../synthetics_service/synthetics_monitor/synthetics_monitor_client';
@ -32,6 +33,7 @@ export interface UMServerRoute<T> {
method: 'GET' | 'PUT' | 'POST' | 'DELETE';
writeAccess?: boolean;
handler: T;
validation?: FullValidationConfig<any, any, any>;
streamHandler?: (
context: UptimeRequestHandlerContext,
request: SyntheticsRequest,
@ -57,13 +59,17 @@ export type UMKibanaRoute = UMRouteDefinition<
export type SyntheticsRestApiRouteFactory<
ClientContract = any,
QueryParams = Record<string, any>
> = () => SyntheticsRoute<ClientContract, QueryParams>;
Params = any,
Query = Record<string, any>,
Body = any
> = () => SyntheticsRoute<ClientContract, Params, Query, Body>;
export type SyntheticsRoute<
ClientContract = unknown,
QueryParams = Record<string, any>
> = UMRouteDefinition<SyntheticsRouteHandler<ClientContract, QueryParams>>;
Params = Record<string, any>,
Query = Record<string, any>,
Body = any
> = UMRouteDefinition<SyntheticsRouteHandler<ClientContract, Params, Query, Body>>;
export type SyntheticsRouteWrapper = (
uptimeRoute: SyntheticsRoute<Record<string, unknown>>,
@ -81,10 +87,14 @@ export interface UptimeRouteContext {
subject?: Subject<unknown>;
}
export interface RouteContext<Query = Record<string, any>> {
export interface RouteContext<
Params = Record<string, any>,
Query = Record<string, any>,
Body = any
> {
uptimeEsClient: UptimeEsClient;
context: UptimeRequestHandlerContext;
request: KibanaRequest<Record<string, any>, Query, Record<string, any>>;
request: KibanaRequest<Params, Query, Body>;
response: KibanaResponseFactory;
savedObjectsClient: SavedObjectsClientContract;
server: SyntheticsServerSetup;
@ -93,7 +103,12 @@ export interface RouteContext<Query = Record<string, any>> {
spaceId: string;
}
export type SyntheticsRouteHandler<ClientContract = unknown, QueryParams = Record<string, any>> = ({
export type SyntheticsRouteHandler<
ClientContract,
Params = Record<string, any>,
Query = Record<string, any>,
Body = any
> = ({
uptimeEsClient,
context,
request,
@ -101,4 +116,4 @@ export type SyntheticsRouteHandler<ClientContract = unknown, QueryParams = Recor
server,
savedObjectsClient,
subject: Subject,
}: RouteContext<QueryParams>) => Promise<IKibanaResponse<ClientContract> | ClientContract>;
}: RouteContext<Params, Query, Body>) => Promise<IKibanaResponse<ClientContract> | ClientContract>;

View file

@ -11,7 +11,7 @@ import { SyntheticsPluginsSetupDependencies, SyntheticsServerSetup } from './typ
import { createSyntheticsRouteWithAuth } from './routes/create_route_with_auth';
import { SyntheticsMonitorClient } from './synthetics_service/synthetics_monitor/synthetics_monitor_client';
import { syntheticsRouteWrapper } from './synthetics_route_wrapper';
import { syntheticsAppRestApiRoutes } from './routes';
import { syntheticsAppPublicRestApiRoutes, syntheticsAppRestApiRoutes } from './routes';
export const initSyntheticsServer = (
server: SyntheticsServerSetup,
@ -19,6 +19,7 @@ export const initSyntheticsServer = (
plugins: SyntheticsPluginsSetupDependencies,
ruleDataClient: IRuleDataClient
) => {
const { router } = server;
syntheticsAppRestApiRoutes.forEach((route) => {
const { method, options, handler, validate, path } = syntheticsRouteWrapper(
createSyntheticsRouteWithAuth(route),
@ -34,16 +35,103 @@ export const initSyntheticsServer = (
switch (method) {
case 'GET':
server.router.get(routeDefinition, handler);
router.get(routeDefinition, handler);
break;
case 'POST':
server.router.post(routeDefinition, handler);
router.post(routeDefinition, handler);
break;
case 'PUT':
server.router.put(routeDefinition, handler);
router.put(routeDefinition, handler);
break;
case 'DELETE':
server.router.delete(routeDefinition, handler);
router.delete(routeDefinition, handler);
break;
default:
throw new Error(`Handler for method ${method} is not defined`);
}
});
syntheticsAppPublicRestApiRoutes.forEach((route) => {
const { method, options, handler, validate, path, validation } = syntheticsRouteWrapper(
createSyntheticsRouteWithAuth(route),
server,
syntheticsMonitorClient
);
const routeDefinition = {
path,
validate,
options,
};
switch (method) {
case 'GET':
router.versioned
.get({
access: 'public',
path: routeDefinition.path,
options: {
tags: options?.tags,
},
})
.addVersion(
{
version: '2023-10-31',
validate: validation ?? false,
},
handler
);
break;
case 'PUT':
router.versioned
.put({
access: 'public',
path: routeDefinition.path,
options: {
tags: options?.tags,
},
})
.addVersion(
{
version: '2023-10-31',
validate: validation ?? false,
},
handler
);
break;
case 'POST':
router.versioned
.post({
access: 'public',
path: routeDefinition.path,
options: {
tags: options?.tags,
},
})
.addVersion(
{
version: '2023-10-31',
validate: validation ?? false,
},
handler
);
break;
case 'DELETE':
router.versioned
.delete({
access: 'public',
path: routeDefinition.path,
options: {
tags: options?.tags,
},
})
.addVersion(
{
version: '2023-10-31',
validate: validation ?? false,
},
handler
);
break;
default:
throw new Error(`Handler for method ${method} is not defined`);

View file

@ -94,9 +94,9 @@ export default function ({ getService }: FtrProviderContext) {
assertHas(param, testParam);
await supertestAPI
.put(SYNTHETICS_API_URLS.PARAMS)
.put(SYNTHETICS_API_URLS.PARAMS + '/' + param.id)
.set('kbn-xsrf', 'true')
.send({ ...expectedUpdatedParam, id: param.id })
.send(expectedUpdatedParam)
.expect(200);
const updatedGetResponse = await supertestAPI
@ -155,9 +155,9 @@ export default function ({ getService }: FtrProviderContext) {
assertHas(param, testParam);
await supertestAPI
.put(`/s/${SPACE_ID}${SYNTHETICS_API_URLS.PARAMS}`)
.put(`/s/${SPACE_ID}${SYNTHETICS_API_URLS.PARAMS}/${param.id}`)
.set('kbn-xsrf', 'true')
.send({ ...expectedUpdatedParam, id: param.id })
.send(expectedUpdatedParam)
.expect(200);
const updatedGetResponse = await supertestAPI
@ -204,9 +204,9 @@ export default function ({ getService }: FtrProviderContext) {
.expect(200);
await supertestAPI
.put(`/s/${SPACE_ID_TWO}${SYNTHETICS_API_URLS.PARAMS}`)
.put(`/s/${SPACE_ID_TWO}${SYNTHETICS_API_URLS.PARAMS}/${param.id}}`)
.set('kbn-xsrf', 'true')
.send({ ...updatedParam, id: param.id })
.send(updatedParam)
.expect(404);
const updatedGetResponse = await supertestAPI
@ -251,9 +251,9 @@ export default function ({ getService }: FtrProviderContext) {
assertHas(param, testParam);
await supertestAPI
.put(`/s/doesnotexist${SYNTHETICS_API_URLS.PARAMS}`)
.put(`/s/doesnotexist${SYNTHETICS_API_URLS.PARAMS}/${param.id}}`)
.set('kbn-xsrf', 'true')
.send({ ...updatedParam, id: param.id })
.send(updatedParam)
.expect(404);
});

View file

@ -22,8 +22,6 @@ import { PrivateLocationTestService } from './services/private_location_test_ser
import { comparePolicies, getTestSyntheticsPolicy } from './sample_data/test_policy';
export default function ({ getService }: FtrProviderContext) {
// FLAKY: https://github.com/elastic/kibana/issues/162594
// Failing: See https://github.com/elastic/kibana/issues/162594
describe('SyncGlobalParams', function () {
this.tags('skipCloud');
const supertestAPI = getService('supertest');
@ -278,8 +276,8 @@ export default function ({ getService }: FtrProviderContext) {
const deleteResponse = await supertestAPI
.delete(SYNTHETICS_API_URLS.PARAMS)
.query({ ids: JSON.stringify(ids) })
.set('kbn-xsrf', 'true')
.send({ ids })
.expect(200);
expect(deleteResponse.body).to.have.length(2);