mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[Core] [UA] Support API Deprecations (#196081)
# Summary Adds a new API deprecations feature inside core. This feature enabled plugin developers to mark their versioned and unversioned public routes as deprecated. These deprecations will be surfaced to the users through UA to help them understand the deprecation and address it before upgrading. This PR also surfaces these deprecations to UA. Closes https://github.com/elastic/kibana/issues/117241 1. Core service to flag deprecated routes 2. UA code to surface and resolve deprecated routes ## Flagging a deprecated Route ### The route deprecation option We have three types of route deprecations: - `type: bump`: A version bump deprecation means the API has a new version and the current version will be removed in the future in favor of the newer version. - `type: remove`: This API will be completely removed. You will no longer be able to use it in the future. - `type: migrate`: This API will be migrated to a different API and will be removed in the future in favor of the other API. All route deprecations expect a documentation link to help users navigate. We might add a generic documentation link and drop this requirement in the future but for now this is required. ### Deprecated Route Example Full examples can be found in the `routing_example` example plugin located in this directory: `examples/routing_example/server/routes/deprecated_routes` ```ts router[versioned?].get( { path: '/', options: { deprecated: { documentationUrl: 'https://google.com', severity: 'warning', reason: { type: 'bump', newApiVersion: '2024-10-13', }, }, }, }, async (context, req, res) => { ... ``` ## Surfaced API deprecations in UA The list of deprecated APIs will be listed inside Kibana deprecations along with the already supported config deprecations. <img width="1728" alt="image" src="https://github.com/user-attachments/assets/5bece704-b80b-4397-8ba2-6235f8995e4a"> Users can click on the list item to learn more about each deprecation and mark it as resolved <img width="1476" alt="image" src="https://github.com/user-attachments/assets/91c9207b-b246-482d-a5e4-21d0c61582a8"> ### Marking as resolved Users can click on mark as resolved button in the UA to hide the deprecation from the Kiban deprecations list. We keep track on when this button was clicked and how many times the API has been called. If the API is called again the deprecation will re-appear inside the list. We might add a feature in the future to permenantly supress the API deprecation from showing in the list through a configuration (https://github.com/elastic/kibana/issues/196089) If the API has been marked as resolved before we show this in the flyout message: > The API GET /api/deprecations/ has been called 25 times. The last time the API was called was on Monday, October 14, 2024 1:08 PM +03:00. > The api has been called 2 times since the last time it was marked as resolved on Monday, October 14, 2024 1:08 PM +03:00 Once marked as resolved the flyout exists and we show this to the user until they refresh the page <img width="1453" alt="image" src="https://github.com/user-attachments/assets/8bb5bc8b-d1a3-478f-9489-23cfa7db6350"> ## Telemetry: We keep track of 2 new things for telemetry purposes: 1. The number of times the deprecated API has been called 2. The number of times the deprecated API has been resolved (how many times the mark as resolved button in UA was clicked) ## Code review - [x] Core team is expected to review the whole PR - [ ] Docs team to review the copy and update the UA displayed texts (title, description, and manual steps) - [x] kibana-management team is expected to review the UA code changes and UI - [ ] A few teams are only required to approve this PR and update their `deprecated: true` route param to the new deprecationInfo object we now expect. There is an issue tracker to address those in separate PRs later on: https://github.com/elastic/kibana/issues/196095 ## Testing Run kibana locally with the test example plugin that has deprecated routes ``` yarn start --plugin-path=examples/routing_example --plugin-path=examples/developer_examples ``` The following comprehensive deprecated routes examples are registered inside the folder: `examples/routing_example/server/routes/deprecated_routes` Run them in the console to trigger the deprecation condition so they show up in the UA: ``` # Versioned routes: Version 1 is deprecated GET kbn:/api/routing_example/d/versioned?apiVersion=1 GET kbn:/api/routing_example/d/versioned?apiVersion=2 # Non-versioned routes GET kbn:/api/routing_example/d/removed_route POST kbn:/api/routing_example/d/migrated_route {} ``` 1. You can also mark as deprecated in the UA to remove the deprecation from the list. 2. Check the telemetry response to see the reported data about the deprecated route. 3. Calling version 2 of the API does not do anything since it is not deprecated unlike version `1` (`GET kbn:/api/routing_example/d/versioned?apiVersion=2`) 4. Internally you can see the deprecations counters from the dev console by running the following: ``` GET .kibana_usage_counters/_search { "query": { "bool": { "should": [ {"match": { "usage-counter.counterType": "deprecated_api_call:total"}}, {"match": { "usage-counter.counterType": "deprecated_api_call:resolved"}}, {"match": { "usage-counter.counterType": "deprecated_api_call:marked_as_resolved"}} ] } } } ``` --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: florent-leborgne <florent.leborgne@elastic.co> Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
parent
1e05086ea4
commit
c417196905
139 changed files with 2057 additions and 381 deletions
|
@ -344,7 +344,7 @@ enabled:
|
|||
- x-pack/test/task_manager_claimer_mget/config.ts
|
||||
- x-pack/test/ui_capabilities/security_and_spaces/config.ts
|
||||
- x-pack/test/ui_capabilities/spaces_only/config.ts
|
||||
- x-pack/test/upgrade_assistant_integration/config.js
|
||||
- x-pack/test/upgrade_assistant_integration/config.ts
|
||||
- x-pack/test/usage_collection/config.ts
|
||||
- x-pack/performance/journeys_e2e/aiops_log_rate_analysis.ts
|
||||
- x-pack/performance/journeys_e2e/ecommerce_dashboard.ts
|
||||
|
|
|
@ -643,6 +643,7 @@ module.exports = {
|
|||
'x-pack/test/*/*config.*ts',
|
||||
'x-pack/test/saved_object_api_integration/*/apis/**/*',
|
||||
'x-pack/test/ui_capabilities/*/tests/**/*',
|
||||
'x-pack/test/upgrade_assistant_integration/**/*',
|
||||
'x-pack/test/performance/**/*.ts',
|
||||
'**/cypress.config.{js,ts}',
|
||||
'x-pack/test_serverless/**/config*.ts',
|
||||
|
|
|
@ -15,3 +15,9 @@ export const POST_MESSAGE_ROUTE_PATH = '/api/post_message';
|
|||
|
||||
// Internal APIs should use the `internal` prefix, instead of the `api` prefix.
|
||||
export const INTERNAL_GET_MESSAGE_BY_ID_ROUTE = '/internal/get_message';
|
||||
|
||||
export const DEPRECATED_ROUTES = {
|
||||
REMOVED_ROUTE: '/api/routing_example/d/removed_route',
|
||||
MIGRATED_ROUTE: '/api/routing_example/d/migrated_route',
|
||||
VERSIONED_ROUTE: '/api/routing_example/d/versioned',
|
||||
};
|
||||
|
|
|
@ -8,13 +8,14 @@
|
|||
*/
|
||||
|
||||
import { Plugin, CoreSetup, CoreStart } from '@kbn/core/server';
|
||||
import { registerRoutes } from './routes';
|
||||
import { registerRoutes, registerDeprecatedRoutes } from './routes';
|
||||
|
||||
export class RoutingExamplePlugin implements Plugin<{}, {}> {
|
||||
public setup(core: CoreSetup) {
|
||||
const router = core.http.createRouter();
|
||||
|
||||
registerRoutes(router);
|
||||
registerDeprecatedRoutes(router);
|
||||
|
||||
return {};
|
||||
}
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
/*
|
||||
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { IRouter } from '@kbn/core/server';
|
||||
import { registerDeprecatedRoute } from './unversioned';
|
||||
import { registerVersionedDeprecatedRoute } from './versioned';
|
||||
|
||||
export function registerDeprecatedRoutes(router: IRouter) {
|
||||
registerDeprecatedRoute(router);
|
||||
registerVersionedDeprecatedRoute(router);
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
/*
|
||||
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import type { IRouter } from '@kbn/core/server';
|
||||
import { schema } from '@kbn/config-schema';
|
||||
import { DEPRECATED_ROUTES } from '../../../common';
|
||||
|
||||
export const registerDeprecatedRoute = (router: IRouter) => {
|
||||
router.get(
|
||||
{
|
||||
path: DEPRECATED_ROUTES.REMOVED_ROUTE,
|
||||
validate: false,
|
||||
options: {
|
||||
access: 'public',
|
||||
deprecated: {
|
||||
documentationUrl: 'https://elastic.co/',
|
||||
severity: 'critical',
|
||||
reason: { type: 'remove' },
|
||||
},
|
||||
},
|
||||
},
|
||||
async (ctx, req, res) => {
|
||||
return res.ok({
|
||||
body: { result: 'Called deprecated route. Check UA to see the deprecation.' },
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
router.post(
|
||||
{
|
||||
path: DEPRECATED_ROUTES.MIGRATED_ROUTE,
|
||||
validate: {
|
||||
body: schema.object({
|
||||
test: schema.maybe(schema.boolean()),
|
||||
}),
|
||||
},
|
||||
options: {
|
||||
access: 'public',
|
||||
deprecated: {
|
||||
documentationUrl: 'https://elastic.co/',
|
||||
severity: 'critical',
|
||||
reason: {
|
||||
type: 'migrate',
|
||||
newApiMethod: 'GET',
|
||||
newApiPath: `${DEPRECATED_ROUTES.VERSIONED_ROUTE}?apiVersion=2`,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
async (ctx, req, res) => {
|
||||
return res.ok({
|
||||
body: { result: 'Called deprecated route. Check UA to see the deprecation.' },
|
||||
});
|
||||
}
|
||||
);
|
||||
};
|
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import type { RequestHandler } from '@kbn/core-http-server';
|
||||
import type { IRouter } from '@kbn/core/server';
|
||||
import { DEPRECATED_ROUTES } from '../../../common';
|
||||
|
||||
const createDummyHandler =
|
||||
(version: string): RequestHandler =>
|
||||
(ctx, req, res) => {
|
||||
return res.ok({ body: { result: `API version ${version}.` } });
|
||||
};
|
||||
|
||||
export const registerVersionedDeprecatedRoute = (router: IRouter) => {
|
||||
const versionedRoute = router.versioned.get({
|
||||
path: DEPRECATED_ROUTES.VERSIONED_ROUTE,
|
||||
description: 'Routing example plugin deprecated versioned route.',
|
||||
access: 'internal',
|
||||
options: {
|
||||
excludeFromOAS: true,
|
||||
},
|
||||
enableQueryVersion: true,
|
||||
});
|
||||
|
||||
versionedRoute.addVersion(
|
||||
{
|
||||
options: {
|
||||
deprecated: {
|
||||
documentationUrl: 'https://elastic.co/',
|
||||
severity: 'warning',
|
||||
reason: { type: 'bump', newApiVersion: '2' },
|
||||
},
|
||||
},
|
||||
validate: false,
|
||||
version: '1',
|
||||
},
|
||||
createDummyHandler('1')
|
||||
);
|
||||
|
||||
versionedRoute.addVersion(
|
||||
{
|
||||
version: '2',
|
||||
validate: false,
|
||||
},
|
||||
createDummyHandler('2')
|
||||
);
|
||||
};
|
|
@ -8,3 +8,4 @@
|
|||
*/
|
||||
|
||||
export { registerRoutes } from './register_routes';
|
||||
export { registerDeprecatedRoutes } from './deprecated_routes';
|
||||
|
|
|
@ -20,5 +20,6 @@
|
|||
"@kbn/core-http-browser",
|
||||
"@kbn/config-schema",
|
||||
"@kbn/react-kibana-context-render",
|
||||
"@kbn/core-http-server",
|
||||
]
|
||||
}
|
||||
|
|
|
@ -6409,7 +6409,6 @@
|
|||
},
|
||||
"/api/fleet/agent-status": {
|
||||
"get": {
|
||||
"deprecated": true,
|
||||
"operationId": "%2Fapi%2Ffleet%2Fagent-status#0",
|
||||
"parameters": [
|
||||
{
|
||||
|
@ -17479,7 +17478,6 @@
|
|||
]
|
||||
},
|
||||
"put": {
|
||||
"deprecated": true,
|
||||
"operationId": "%2Fapi%2Ffleet%2Fagents%2F%7BagentId%7D%2Freassign#0",
|
||||
"parameters": [
|
||||
{
|
||||
|
@ -18179,7 +18177,6 @@
|
|||
},
|
||||
"/api/fleet/enrollment-api-keys": {
|
||||
"get": {
|
||||
"deprecated": true,
|
||||
"operationId": "%2Fapi%2Ffleet%2Fenrollment-api-keys#0",
|
||||
"parameters": [
|
||||
{
|
||||
|
@ -18226,7 +18223,6 @@
|
|||
"tags": []
|
||||
},
|
||||
"post": {
|
||||
"deprecated": true,
|
||||
"operationId": "%2Fapi%2Ffleet%2Fenrollment-api-keys#1",
|
||||
"parameters": [
|
||||
{
|
||||
|
@ -18283,7 +18279,6 @@
|
|||
},
|
||||
"/api/fleet/enrollment-api-keys/{keyId}": {
|
||||
"delete": {
|
||||
"deprecated": true,
|
||||
"operationId": "%2Fapi%2Ffleet%2Fenrollment-api-keys%2F%7BkeyId%7D#1",
|
||||
"parameters": [
|
||||
{
|
||||
|
@ -18322,7 +18317,6 @@
|
|||
"tags": []
|
||||
},
|
||||
"get": {
|
||||
"deprecated": true,
|
||||
"operationId": "%2Fapi%2Ffleet%2Fenrollment-api-keys%2F%7BkeyId%7D#0",
|
||||
"parameters": [
|
||||
{
|
||||
|
@ -25053,7 +25047,6 @@
|
|||
},
|
||||
"/api/fleet/epm/packages/{pkgkey}": {
|
||||
"delete": {
|
||||
"deprecated": true,
|
||||
"operationId": "%2Fapi%2Ffleet%2Fepm%2Fpackages%2F%7Bpkgkey%7D#3",
|
||||
"parameters": [
|
||||
{
|
||||
|
@ -25111,7 +25104,6 @@
|
|||
"tags": []
|
||||
},
|
||||
"get": {
|
||||
"deprecated": true,
|
||||
"operationId": "%2Fapi%2Ffleet%2Fepm%2Fpackages%2F%7Bpkgkey%7D#0",
|
||||
"parameters": [
|
||||
{
|
||||
|
@ -25173,7 +25165,6 @@
|
|||
"tags": []
|
||||
},
|
||||
"post": {
|
||||
"deprecated": true,
|
||||
"operationId": "%2Fapi%2Ffleet%2Fepm%2Fpackages%2F%7Bpkgkey%7D#2",
|
||||
"parameters": [
|
||||
{
|
||||
|
@ -25257,7 +25248,6 @@
|
|||
"tags": []
|
||||
},
|
||||
"put": {
|
||||
"deprecated": true,
|
||||
"operationId": "%2Fapi%2Ffleet%2Fepm%2Fpackages%2F%7Bpkgkey%7D#1",
|
||||
"parameters": [
|
||||
{
|
||||
|
@ -40472,7 +40462,6 @@
|
|||
},
|
||||
"/api/fleet/service-tokens": {
|
||||
"post": {
|
||||
"deprecated": true,
|
||||
"description": "Create a service token",
|
||||
"operationId": "%2Fapi%2Ffleet%2Fservice-tokens#0",
|
||||
"parameters": [
|
||||
|
|
|
@ -6409,7 +6409,6 @@
|
|||
},
|
||||
"/api/fleet/agent-status": {
|
||||
"get": {
|
||||
"deprecated": true,
|
||||
"operationId": "%2Fapi%2Ffleet%2Fagent-status#0",
|
||||
"parameters": [
|
||||
{
|
||||
|
@ -17479,7 +17478,6 @@
|
|||
]
|
||||
},
|
||||
"put": {
|
||||
"deprecated": true,
|
||||
"operationId": "%2Fapi%2Ffleet%2Fagents%2F%7BagentId%7D%2Freassign#0",
|
||||
"parameters": [
|
||||
{
|
||||
|
@ -18179,7 +18177,6 @@
|
|||
},
|
||||
"/api/fleet/enrollment-api-keys": {
|
||||
"get": {
|
||||
"deprecated": true,
|
||||
"operationId": "%2Fapi%2Ffleet%2Fenrollment-api-keys#0",
|
||||
"parameters": [
|
||||
{
|
||||
|
@ -18226,7 +18223,6 @@
|
|||
"tags": []
|
||||
},
|
||||
"post": {
|
||||
"deprecated": true,
|
||||
"operationId": "%2Fapi%2Ffleet%2Fenrollment-api-keys#1",
|
||||
"parameters": [
|
||||
{
|
||||
|
@ -18283,7 +18279,6 @@
|
|||
},
|
||||
"/api/fleet/enrollment-api-keys/{keyId}": {
|
||||
"delete": {
|
||||
"deprecated": true,
|
||||
"operationId": "%2Fapi%2Ffleet%2Fenrollment-api-keys%2F%7BkeyId%7D#1",
|
||||
"parameters": [
|
||||
{
|
||||
|
@ -18322,7 +18317,6 @@
|
|||
"tags": []
|
||||
},
|
||||
"get": {
|
||||
"deprecated": true,
|
||||
"operationId": "%2Fapi%2Ffleet%2Fenrollment-api-keys%2F%7BkeyId%7D#0",
|
||||
"parameters": [
|
||||
{
|
||||
|
@ -25053,7 +25047,6 @@
|
|||
},
|
||||
"/api/fleet/epm/packages/{pkgkey}": {
|
||||
"delete": {
|
||||
"deprecated": true,
|
||||
"operationId": "%2Fapi%2Ffleet%2Fepm%2Fpackages%2F%7Bpkgkey%7D#3",
|
||||
"parameters": [
|
||||
{
|
||||
|
@ -25111,7 +25104,6 @@
|
|||
"tags": []
|
||||
},
|
||||
"get": {
|
||||
"deprecated": true,
|
||||
"operationId": "%2Fapi%2Ffleet%2Fepm%2Fpackages%2F%7Bpkgkey%7D#0",
|
||||
"parameters": [
|
||||
{
|
||||
|
@ -25173,7 +25165,6 @@
|
|||
"tags": []
|
||||
},
|
||||
"post": {
|
||||
"deprecated": true,
|
||||
"operationId": "%2Fapi%2Ffleet%2Fepm%2Fpackages%2F%7Bpkgkey%7D#2",
|
||||
"parameters": [
|
||||
{
|
||||
|
@ -25257,7 +25248,6 @@
|
|||
"tags": []
|
||||
},
|
||||
"put": {
|
||||
"deprecated": true,
|
||||
"operationId": "%2Fapi%2Ffleet%2Fepm%2Fpackages%2F%7Bpkgkey%7D#1",
|
||||
"parameters": [
|
||||
{
|
||||
|
@ -40472,7 +40462,6 @@
|
|||
},
|
||||
"/api/fleet/service-tokens": {
|
||||
"post": {
|
||||
"deprecated": true,
|
||||
"description": "Create a service token",
|
||||
"operationId": "%2Fapi%2Ffleet%2Fservice-tokens#0",
|
||||
"parameters": [
|
||||
|
|
|
@ -15066,7 +15066,6 @@ paths:
|
|||
- Elastic Agents
|
||||
/api/fleet/agent-status:
|
||||
get:
|
||||
deprecated: true
|
||||
operationId: '%2Fapi%2Ffleet%2Fagent-status#0'
|
||||
parameters:
|
||||
- description: The version of the API to use
|
||||
|
@ -16769,7 +16768,6 @@ paths:
|
|||
tags:
|
||||
- Elastic Agent actions
|
||||
put:
|
||||
deprecated: true
|
||||
operationId: '%2Fapi%2Ffleet%2Fagents%2F%7BagentId%7D%2Freassign#0'
|
||||
parameters:
|
||||
- description: The version of the API to use
|
||||
|
@ -18622,7 +18620,6 @@ paths:
|
|||
- Fleet enrollment API keys
|
||||
/api/fleet/enrollment-api-keys:
|
||||
get:
|
||||
deprecated: true
|
||||
operationId: '%2Fapi%2Ffleet%2Fenrollment-api-keys#0'
|
||||
parameters:
|
||||
- description: The version of the API to use
|
||||
|
@ -18654,7 +18651,6 @@ paths:
|
|||
summary: ''
|
||||
tags: []
|
||||
post:
|
||||
deprecated: true
|
||||
operationId: '%2Fapi%2Ffleet%2Fenrollment-api-keys#1'
|
||||
parameters:
|
||||
- description: The version of the API to use
|
||||
|
@ -18692,7 +18688,6 @@ paths:
|
|||
tags: []
|
||||
/api/fleet/enrollment-api-keys/{keyId}:
|
||||
delete:
|
||||
deprecated: true
|
||||
operationId: '%2Fapi%2Ffleet%2Fenrollment-api-keys%2F%7BkeyId%7D#1'
|
||||
parameters:
|
||||
- description: The version of the API to use
|
||||
|
@ -18719,7 +18714,6 @@ paths:
|
|||
summary: ''
|
||||
tags: []
|
||||
get:
|
||||
deprecated: true
|
||||
operationId: '%2Fapi%2Ffleet%2Fenrollment-api-keys%2F%7BkeyId%7D#0'
|
||||
parameters:
|
||||
- description: The version of the API to use
|
||||
|
@ -20434,7 +20428,6 @@ paths:
|
|||
- Elastic Package Manager (EPM)
|
||||
/api/fleet/epm/packages/{pkgkey}:
|
||||
delete:
|
||||
deprecated: true
|
||||
operationId: '%2Fapi%2Ffleet%2Fepm%2Fpackages%2F%7Bpkgkey%7D#3'
|
||||
parameters:
|
||||
- description: The version of the API to use
|
||||
|
@ -20473,7 +20466,6 @@ paths:
|
|||
summary: ''
|
||||
tags: []
|
||||
get:
|
||||
deprecated: true
|
||||
operationId: '%2Fapi%2Ffleet%2Fepm%2Fpackages%2F%7Bpkgkey%7D#0'
|
||||
parameters:
|
||||
- description: The version of the API to use
|
||||
|
@ -20514,7 +20506,6 @@ paths:
|
|||
summary: ''
|
||||
tags: []
|
||||
post:
|
||||
deprecated: true
|
||||
operationId: '%2Fapi%2Ffleet%2Fepm%2Fpackages%2F%7Bpkgkey%7D#2'
|
||||
parameters:
|
||||
- description: The version of the API to use
|
||||
|
@ -20570,7 +20561,6 @@ paths:
|
|||
summary: ''
|
||||
tags: []
|
||||
put:
|
||||
deprecated: true
|
||||
operationId: '%2Fapi%2Ffleet%2Fepm%2Fpackages%2F%7Bpkgkey%7D#1'
|
||||
parameters:
|
||||
- description: The version of the API to use
|
||||
|
@ -33533,7 +33523,6 @@ paths:
|
|||
- Fleet service tokens
|
||||
/api/fleet/service-tokens:
|
||||
post:
|
||||
deprecated: true
|
||||
description: Create a service token
|
||||
operationId: '%2Fapi%2Ffleet%2Fservice-tokens#0'
|
||||
parameters:
|
||||
|
|
|
@ -15066,7 +15066,6 @@ paths:
|
|||
- Elastic Agents
|
||||
/api/fleet/agent-status:
|
||||
get:
|
||||
deprecated: true
|
||||
operationId: '%2Fapi%2Ffleet%2Fagent-status#0'
|
||||
parameters:
|
||||
- description: The version of the API to use
|
||||
|
@ -16769,7 +16768,6 @@ paths:
|
|||
tags:
|
||||
- Elastic Agent actions
|
||||
put:
|
||||
deprecated: true
|
||||
operationId: '%2Fapi%2Ffleet%2Fagents%2F%7BagentId%7D%2Freassign#0'
|
||||
parameters:
|
||||
- description: The version of the API to use
|
||||
|
@ -18622,7 +18620,6 @@ paths:
|
|||
- Fleet enrollment API keys
|
||||
/api/fleet/enrollment-api-keys:
|
||||
get:
|
||||
deprecated: true
|
||||
operationId: '%2Fapi%2Ffleet%2Fenrollment-api-keys#0'
|
||||
parameters:
|
||||
- description: The version of the API to use
|
||||
|
@ -18654,7 +18651,6 @@ paths:
|
|||
summary: ''
|
||||
tags: []
|
||||
post:
|
||||
deprecated: true
|
||||
operationId: '%2Fapi%2Ffleet%2Fenrollment-api-keys#1'
|
||||
parameters:
|
||||
- description: The version of the API to use
|
||||
|
@ -18692,7 +18688,6 @@ paths:
|
|||
tags: []
|
||||
/api/fleet/enrollment-api-keys/{keyId}:
|
||||
delete:
|
||||
deprecated: true
|
||||
operationId: '%2Fapi%2Ffleet%2Fenrollment-api-keys%2F%7BkeyId%7D#1'
|
||||
parameters:
|
||||
- description: The version of the API to use
|
||||
|
@ -18719,7 +18714,6 @@ paths:
|
|||
summary: ''
|
||||
tags: []
|
||||
get:
|
||||
deprecated: true
|
||||
operationId: '%2Fapi%2Ffleet%2Fenrollment-api-keys%2F%7BkeyId%7D#0'
|
||||
parameters:
|
||||
- description: The version of the API to use
|
||||
|
@ -20434,7 +20428,6 @@ paths:
|
|||
- Elastic Package Manager (EPM)
|
||||
/api/fleet/epm/packages/{pkgkey}:
|
||||
delete:
|
||||
deprecated: true
|
||||
operationId: '%2Fapi%2Ffleet%2Fepm%2Fpackages%2F%7Bpkgkey%7D#3'
|
||||
parameters:
|
||||
- description: The version of the API to use
|
||||
|
@ -20473,7 +20466,6 @@ paths:
|
|||
summary: ''
|
||||
tags: []
|
||||
get:
|
||||
deprecated: true
|
||||
operationId: '%2Fapi%2Ffleet%2Fepm%2Fpackages%2F%7Bpkgkey%7D#0'
|
||||
parameters:
|
||||
- description: The version of the API to use
|
||||
|
@ -20514,7 +20506,6 @@ paths:
|
|||
summary: ''
|
||||
tags: []
|
||||
post:
|
||||
deprecated: true
|
||||
operationId: '%2Fapi%2Ffleet%2Fepm%2Fpackages%2F%7Bpkgkey%7D#2'
|
||||
parameters:
|
||||
- description: The version of the API to use
|
||||
|
@ -20570,7 +20561,6 @@ paths:
|
|||
summary: ''
|
||||
tags: []
|
||||
put:
|
||||
deprecated: true
|
||||
operationId: '%2Fapi%2Ffleet%2Fepm%2Fpackages%2F%7Bpkgkey%7D#1'
|
||||
parameters:
|
||||
- description: The version of the API to use
|
||||
|
@ -33533,7 +33523,6 @@ paths:
|
|||
- Fleet service tokens
|
||||
/api/fleet/service-tokens:
|
||||
post:
|
||||
deprecated: true
|
||||
description: Create a service token
|
||||
operationId: '%2Fapi%2Ffleet%2Fservice-tokens#0'
|
||||
parameters:
|
||||
|
|
|
@ -18495,7 +18495,6 @@ paths:
|
|||
- Elastic Agents
|
||||
/api/fleet/agent-status:
|
||||
get:
|
||||
deprecated: true
|
||||
operationId: '%2Fapi%2Ffleet%2Fagent-status#0'
|
||||
parameters:
|
||||
- description: The version of the API to use
|
||||
|
@ -20198,7 +20197,6 @@ paths:
|
|||
tags:
|
||||
- Elastic Agent actions
|
||||
put:
|
||||
deprecated: true
|
||||
operationId: '%2Fapi%2Ffleet%2Fagents%2F%7BagentId%7D%2Freassign#0'
|
||||
parameters:
|
||||
- description: The version of the API to use
|
||||
|
@ -22051,7 +22049,6 @@ paths:
|
|||
- Fleet enrollment API keys
|
||||
/api/fleet/enrollment-api-keys:
|
||||
get:
|
||||
deprecated: true
|
||||
operationId: '%2Fapi%2Ffleet%2Fenrollment-api-keys#0'
|
||||
parameters:
|
||||
- description: The version of the API to use
|
||||
|
@ -22083,7 +22080,6 @@ paths:
|
|||
summary: ''
|
||||
tags: []
|
||||
post:
|
||||
deprecated: true
|
||||
operationId: '%2Fapi%2Ffleet%2Fenrollment-api-keys#1'
|
||||
parameters:
|
||||
- description: The version of the API to use
|
||||
|
@ -22121,7 +22117,6 @@ paths:
|
|||
tags: []
|
||||
/api/fleet/enrollment-api-keys/{keyId}:
|
||||
delete:
|
||||
deprecated: true
|
||||
operationId: '%2Fapi%2Ffleet%2Fenrollment-api-keys%2F%7BkeyId%7D#1'
|
||||
parameters:
|
||||
- description: The version of the API to use
|
||||
|
@ -22148,7 +22143,6 @@ paths:
|
|||
summary: ''
|
||||
tags: []
|
||||
get:
|
||||
deprecated: true
|
||||
operationId: '%2Fapi%2Ffleet%2Fenrollment-api-keys%2F%7BkeyId%7D#0'
|
||||
parameters:
|
||||
- description: The version of the API to use
|
||||
|
@ -23863,7 +23857,6 @@ paths:
|
|||
- Elastic Package Manager (EPM)
|
||||
/api/fleet/epm/packages/{pkgkey}:
|
||||
delete:
|
||||
deprecated: true
|
||||
operationId: '%2Fapi%2Ffleet%2Fepm%2Fpackages%2F%7Bpkgkey%7D#3'
|
||||
parameters:
|
||||
- description: The version of the API to use
|
||||
|
@ -23902,7 +23895,6 @@ paths:
|
|||
summary: ''
|
||||
tags: []
|
||||
get:
|
||||
deprecated: true
|
||||
operationId: '%2Fapi%2Ffleet%2Fepm%2Fpackages%2F%7Bpkgkey%7D#0'
|
||||
parameters:
|
||||
- description: The version of the API to use
|
||||
|
@ -23943,7 +23935,6 @@ paths:
|
|||
summary: ''
|
||||
tags: []
|
||||
post:
|
||||
deprecated: true
|
||||
operationId: '%2Fapi%2Ffleet%2Fepm%2Fpackages%2F%7Bpkgkey%7D#2'
|
||||
parameters:
|
||||
- description: The version of the API to use
|
||||
|
@ -23999,7 +23990,6 @@ paths:
|
|||
summary: ''
|
||||
tags: []
|
||||
put:
|
||||
deprecated: true
|
||||
operationId: '%2Fapi%2Ffleet%2Fepm%2Fpackages%2F%7Bpkgkey%7D#1'
|
||||
parameters:
|
||||
- description: The version of the API to use
|
||||
|
@ -36962,7 +36952,6 @@ paths:
|
|||
- Fleet service tokens
|
||||
/api/fleet/service-tokens:
|
||||
post:
|
||||
deprecated: true
|
||||
description: Create a service token
|
||||
operationId: '%2Fapi%2Ffleet%2Fservice-tokens#0'
|
||||
parameters:
|
||||
|
|
|
@ -18495,7 +18495,6 @@ paths:
|
|||
- Elastic Agents
|
||||
/api/fleet/agent-status:
|
||||
get:
|
||||
deprecated: true
|
||||
operationId: '%2Fapi%2Ffleet%2Fagent-status#0'
|
||||
parameters:
|
||||
- description: The version of the API to use
|
||||
|
@ -20198,7 +20197,6 @@ paths:
|
|||
tags:
|
||||
- Elastic Agent actions
|
||||
put:
|
||||
deprecated: true
|
||||
operationId: '%2Fapi%2Ffleet%2Fagents%2F%7BagentId%7D%2Freassign#0'
|
||||
parameters:
|
||||
- description: The version of the API to use
|
||||
|
@ -22051,7 +22049,6 @@ paths:
|
|||
- Fleet enrollment API keys
|
||||
/api/fleet/enrollment-api-keys:
|
||||
get:
|
||||
deprecated: true
|
||||
operationId: '%2Fapi%2Ffleet%2Fenrollment-api-keys#0'
|
||||
parameters:
|
||||
- description: The version of the API to use
|
||||
|
@ -22083,7 +22080,6 @@ paths:
|
|||
summary: ''
|
||||
tags: []
|
||||
post:
|
||||
deprecated: true
|
||||
operationId: '%2Fapi%2Ffleet%2Fenrollment-api-keys#1'
|
||||
parameters:
|
||||
- description: The version of the API to use
|
||||
|
@ -22121,7 +22117,6 @@ paths:
|
|||
tags: []
|
||||
/api/fleet/enrollment-api-keys/{keyId}:
|
||||
delete:
|
||||
deprecated: true
|
||||
operationId: '%2Fapi%2Ffleet%2Fenrollment-api-keys%2F%7BkeyId%7D#1'
|
||||
parameters:
|
||||
- description: The version of the API to use
|
||||
|
@ -22148,7 +22143,6 @@ paths:
|
|||
summary: ''
|
||||
tags: []
|
||||
get:
|
||||
deprecated: true
|
||||
operationId: '%2Fapi%2Ffleet%2Fenrollment-api-keys%2F%7BkeyId%7D#0'
|
||||
parameters:
|
||||
- description: The version of the API to use
|
||||
|
@ -23863,7 +23857,6 @@ paths:
|
|||
- Elastic Package Manager (EPM)
|
||||
/api/fleet/epm/packages/{pkgkey}:
|
||||
delete:
|
||||
deprecated: true
|
||||
operationId: '%2Fapi%2Ffleet%2Fepm%2Fpackages%2F%7Bpkgkey%7D#3'
|
||||
parameters:
|
||||
- description: The version of the API to use
|
||||
|
@ -23902,7 +23895,6 @@ paths:
|
|||
summary: ''
|
||||
tags: []
|
||||
get:
|
||||
deprecated: true
|
||||
operationId: '%2Fapi%2Ffleet%2Fepm%2Fpackages%2F%7Bpkgkey%7D#0'
|
||||
parameters:
|
||||
- description: The version of the API to use
|
||||
|
@ -23943,7 +23935,6 @@ paths:
|
|||
summary: ''
|
||||
tags: []
|
||||
post:
|
||||
deprecated: true
|
||||
operationId: '%2Fapi%2Ffleet%2Fepm%2Fpackages%2F%7Bpkgkey%7D#2'
|
||||
parameters:
|
||||
- description: The version of the API to use
|
||||
|
@ -23999,7 +23990,6 @@ paths:
|
|||
summary: ''
|
||||
tags: []
|
||||
put:
|
||||
deprecated: true
|
||||
operationId: '%2Fapi%2Ffleet%2Fepm%2Fpackages%2F%7Bpkgkey%7D#1'
|
||||
parameters:
|
||||
- description: The version of the API to use
|
||||
|
@ -36962,7 +36952,6 @@ paths:
|
|||
- Fleet service tokens
|
||||
/api/fleet/service-tokens:
|
||||
post:
|
||||
deprecated: true
|
||||
description: Create a service token
|
||||
operationId: '%2Fapi%2Ffleet%2Fservice-tokens#0'
|
||||
parameters:
|
||||
|
|
|
@ -135,7 +135,7 @@ describe('DeprecationsClient', () => {
|
|||
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"reason": "This deprecation cannot be resolved automatically.",
|
||||
"reason": "This deprecation cannot be resolved automatically or marked as resolved.",
|
||||
"status": "fail",
|
||||
}
|
||||
`);
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import type { HttpStart } from '@kbn/core-http-browser';
|
||||
import type { HttpFetchOptionsWithPath, HttpStart } from '@kbn/core-http-browser';
|
||||
import type {
|
||||
DomainDeprecationDetails,
|
||||
DeprecationsGetResponse,
|
||||
|
@ -47,23 +47,15 @@ export class DeprecationsClient {
|
|||
return typeof details.correctiveActions.api === 'object';
|
||||
};
|
||||
|
||||
public resolveDeprecation = async (
|
||||
private getResolveFetchDetails = (
|
||||
details: DomainDeprecationDetails
|
||||
): Promise<ResolveDeprecationResponse> => {
|
||||
): HttpFetchOptionsWithPath | undefined => {
|
||||
const { domainId, correctiveActions } = details;
|
||||
// explicit check required for TS type guard
|
||||
if (typeof correctiveActions.api !== 'object') {
|
||||
return {
|
||||
status: 'fail',
|
||||
reason: i18n.translate('core.deprecations.noCorrectiveAction', {
|
||||
defaultMessage: 'This deprecation cannot be resolved automatically.',
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
const { body, method, path, omitContextFromBody = false } = correctiveActions.api;
|
||||
try {
|
||||
await this.http.fetch<void>({
|
||||
if (correctiveActions.api) {
|
||||
const { body, method, path, omitContextFromBody = false } = correctiveActions.api;
|
||||
|
||||
return {
|
||||
path,
|
||||
method,
|
||||
asSystemRequest: true,
|
||||
|
@ -71,7 +63,54 @@ export class DeprecationsClient {
|
|||
...body,
|
||||
...(omitContextFromBody ? {} : { deprecationDetails: { domainId } }),
|
||||
}),
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
if (correctiveActions.mark_as_resolved_api) {
|
||||
const { routeMethod, routePath, routeVersion, apiTotalCalls, totalMarkedAsResolved } =
|
||||
correctiveActions.mark_as_resolved_api;
|
||||
const incrementBy = apiTotalCalls - totalMarkedAsResolved;
|
||||
|
||||
return {
|
||||
path: '/api/deprecations/mark_as_resolved',
|
||||
method: 'POST',
|
||||
asSystemRequest: true,
|
||||
body: JSON.stringify({
|
||||
domainId,
|
||||
routeMethod,
|
||||
routePath,
|
||||
routeVersion,
|
||||
incrementBy,
|
||||
}),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
public resolveDeprecation = async (
|
||||
details: DomainDeprecationDetails
|
||||
): Promise<ResolveDeprecationResponse> => {
|
||||
const { correctiveActions } = details;
|
||||
const noCorrectiveActionFail = {
|
||||
status: 'fail' as const,
|
||||
reason: i18n.translate('core.deprecations.noCorrectiveAction', {
|
||||
defaultMessage: 'This deprecation cannot be resolved automatically or marked as resolved.',
|
||||
}),
|
||||
};
|
||||
|
||||
if (
|
||||
typeof correctiveActions.api !== 'object' &&
|
||||
typeof correctiveActions.mark_as_resolved_api !== 'object'
|
||||
) {
|
||||
return noCorrectiveActionFail;
|
||||
}
|
||||
|
||||
try {
|
||||
const fetchParams = this.getResolveFetchDetails(details);
|
||||
if (!fetchParams) {
|
||||
return noCorrectiveActionFail;
|
||||
}
|
||||
|
||||
await this.http.fetch<void>(fetchParams);
|
||||
return { status: 'ok' };
|
||||
} catch (err) {
|
||||
return {
|
||||
|
|
|
@ -22,7 +22,7 @@ export interface BaseDeprecationDetails {
|
|||
* The description message to be displayed for the deprecation.
|
||||
* Check the README for writing deprecations in `src/core/server/deprecations/README.mdx`
|
||||
*/
|
||||
message: string;
|
||||
message: string | string[];
|
||||
/**
|
||||
* levels:
|
||||
* - warning: will not break deployment upon upgrade
|
||||
|
@ -39,7 +39,7 @@ export interface BaseDeprecationDetails {
|
|||
* Predefined types are necessary to reduce having similar definitions with different keywords
|
||||
* across kibana deprecations.
|
||||
*/
|
||||
deprecationType?: 'config' | 'feature';
|
||||
deprecationType?: 'config' | 'api' | 'feature';
|
||||
/** (optional) link to the documentation for more details on the deprecation. */
|
||||
documentationUrl?: string;
|
||||
/** (optional) specify the fix for this deprecation requires a full kibana restart. */
|
||||
|
@ -70,9 +70,31 @@ export interface BaseDeprecationDetails {
|
|||
* Check the README for writing deprecations in `src/core/server/deprecations/README.mdx`
|
||||
*/
|
||||
manualSteps: string[];
|
||||
/**
|
||||
* (optional) The api to be called to mark the deprecation as resolved
|
||||
* This corrective action when called should not resolve the deprecation
|
||||
* instead it helps users track manually deprecated apis
|
||||
* If the API used does resolve the deprecation use `correctiveActions.api`
|
||||
*/
|
||||
mark_as_resolved_api?: {
|
||||
apiTotalCalls: number;
|
||||
totalMarkedAsResolved: number;
|
||||
timestamp: Date | number | string;
|
||||
routePath: string;
|
||||
routeMethod: string;
|
||||
routeVersion?: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface ApiDeprecationDetails extends BaseDeprecationDetails {
|
||||
apiId: string;
|
||||
deprecationType: 'api';
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
|
@ -91,7 +113,10 @@ export interface FeatureDeprecationDetails extends BaseDeprecationDetails {
|
|||
/**
|
||||
* @public
|
||||
*/
|
||||
export type DeprecationsDetails = ConfigDeprecationDetails | FeatureDeprecationDetails;
|
||||
export type DeprecationsDetails =
|
||||
| ConfigDeprecationDetails
|
||||
| ApiDeprecationDetails
|
||||
| FeatureDeprecationDetails;
|
||||
|
||||
/**
|
||||
* @public
|
||||
|
|
|
@ -0,0 +1,353 @@
|
|||
/*
|
||||
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import type { DeepPartial } from '@kbn/utility-types';
|
||||
import { mockDeprecationsRegistry, mockDeprecationsFactory } from '../mocks';
|
||||
import {
|
||||
registerApiDeprecationsInfo,
|
||||
buildApiDeprecationId,
|
||||
createGetApiDeprecations,
|
||||
} from './api_deprecations';
|
||||
import { RouterDeprecatedRouteDetails } from '@kbn/core-http-server';
|
||||
import { httpServiceMock } from '@kbn/core-http-server-mocks';
|
||||
import {
|
||||
coreUsageDataServiceMock,
|
||||
coreUsageStatsClientMock,
|
||||
} from '@kbn/core-usage-data-server-mocks';
|
||||
import _ from 'lodash';
|
||||
import { CoreDeprecatedApiUsageStats } from '@kbn/core-usage-data-server';
|
||||
|
||||
describe('#registerApiDeprecationsInfo', () => {
|
||||
const deprecationsFactory = mockDeprecationsFactory.create();
|
||||
const deprecationsRegistry = mockDeprecationsRegistry.create();
|
||||
let usageClientMock: ReturnType<typeof coreUsageStatsClientMock.create>;
|
||||
let http: ReturnType<typeof httpServiceMock.createInternalSetupContract>;
|
||||
let coreUsageData: ReturnType<typeof coreUsageDataServiceMock.createSetupContract>;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
usageClientMock = coreUsageStatsClientMock.create();
|
||||
http = httpServiceMock.createInternalSetupContract();
|
||||
coreUsageData = coreUsageDataServiceMock.createSetupContract(usageClientMock);
|
||||
});
|
||||
|
||||
beforeAll(() => {
|
||||
jest.useFakeTimers().setSystemTime(new Date('2024-10-17T12:06:41.224Z'));
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it('registers api deprecations', async () => {
|
||||
deprecationsFactory.getRegistry.mockReturnValue(deprecationsRegistry);
|
||||
registerApiDeprecationsInfo({ deprecationsFactory, coreUsageData, http });
|
||||
|
||||
expect(deprecationsFactory.getRegistry).toBeCalledWith('core.api_deprecations');
|
||||
expect(deprecationsRegistry.registerDeprecations).toBeCalledTimes(1);
|
||||
expect(deprecationsRegistry.registerDeprecations).toBeCalledWith({
|
||||
getDeprecations: expect.any(Function),
|
||||
});
|
||||
});
|
||||
|
||||
describe('#createGetApiDeprecations', () => {
|
||||
const createDeprecatedRouteDetails = (
|
||||
overrides?: DeepPartial<RouterDeprecatedRouteDetails>
|
||||
): RouterDeprecatedRouteDetails =>
|
||||
_.merge(
|
||||
{
|
||||
routeDeprecationOptions: {
|
||||
documentationUrl: 'https://fake-url',
|
||||
severity: 'critical',
|
||||
reason: {
|
||||
type: 'remove',
|
||||
},
|
||||
},
|
||||
routeMethod: 'get',
|
||||
routePath: '/api/test/',
|
||||
routeVersion: '123',
|
||||
} as RouterDeprecatedRouteDetails,
|
||||
overrides
|
||||
);
|
||||
|
||||
const createApiUsageStat = (
|
||||
apiId: string,
|
||||
overrides?: DeepPartial<CoreDeprecatedApiUsageStats>
|
||||
): CoreDeprecatedApiUsageStats =>
|
||||
_.merge(
|
||||
{
|
||||
apiId,
|
||||
totalMarkedAsResolved: 1,
|
||||
markedAsResolvedLastCalledAt: '2024-10-17T12:06:41.224Z',
|
||||
apiTotalCalls: 13,
|
||||
apiLastCalledAt: '2024-09-01T10:06:41.224Z',
|
||||
},
|
||||
overrides
|
||||
);
|
||||
|
||||
it('returns removed type deprecated route', async () => {
|
||||
const getDeprecations = createGetApiDeprecations({ coreUsageData, http });
|
||||
const deprecatedRoute = createDeprecatedRouteDetails({
|
||||
routePath: '/api/test_removed/',
|
||||
routeDeprecationOptions: { reason: { type: 'remove' } },
|
||||
});
|
||||
http.getRegisteredDeprecatedApis.mockReturnValue([deprecatedRoute]);
|
||||
usageClientMock.getDeprecatedApiUsageStats.mockResolvedValue([
|
||||
createApiUsageStat(buildApiDeprecationId(deprecatedRoute)),
|
||||
]);
|
||||
|
||||
const deprecations = await getDeprecations();
|
||||
expect(deprecations).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"apiId": "123|get|/api/test_removed",
|
||||
"correctiveActions": Object {
|
||||
"manualSteps": Array [
|
||||
"Identify the origin of these API calls.",
|
||||
"This API no longer exists and no replacement is available. Delete any requests you have that use this API.",
|
||||
"Check that you are no longer using the old API in any requests, and mark this issue as resolved. It will no longer appear in the Upgrade Assistant unless another call using this API is detected.",
|
||||
],
|
||||
"mark_as_resolved_api": Object {
|
||||
"apiTotalCalls": 13,
|
||||
"routeMethod": "get",
|
||||
"routePath": "/api/test_removed/",
|
||||
"routeVersion": "123",
|
||||
"timestamp": 2024-10-17T12:06:41.224Z,
|
||||
"totalMarkedAsResolved": 1,
|
||||
},
|
||||
},
|
||||
"deprecationType": "api",
|
||||
"documentationUrl": "https://fake-url",
|
||||
"domainId": "core.routes-deprecations",
|
||||
"level": "critical",
|
||||
"message": Array [
|
||||
"The API \\"GET /api/test_removed/\\" has been called 13 times. The last call was on Sunday, September 1, 2024 6:06 AM -04:00.",
|
||||
"This issue has been marked as resolved on Thursday, October 17, 2024 8:06 AM -04:00 but the API has been called 12 times since.",
|
||||
],
|
||||
"title": "The \\"GET /api/test_removed/\\" route is removed",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('returns migrated type deprecated route', async () => {
|
||||
const getDeprecations = createGetApiDeprecations({ coreUsageData, http });
|
||||
const deprecatedRoute = createDeprecatedRouteDetails({
|
||||
routePath: '/api/test_migrated/',
|
||||
routeDeprecationOptions: {
|
||||
reason: { type: 'migrate', newApiMethod: 'post', newApiPath: '/api/new_path' },
|
||||
},
|
||||
});
|
||||
http.getRegisteredDeprecatedApis.mockReturnValue([deprecatedRoute]);
|
||||
usageClientMock.getDeprecatedApiUsageStats.mockResolvedValue([
|
||||
createApiUsageStat(buildApiDeprecationId(deprecatedRoute)),
|
||||
]);
|
||||
|
||||
const deprecations = await getDeprecations();
|
||||
expect(deprecations).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"apiId": "123|get|/api/test_migrated",
|
||||
"correctiveActions": Object {
|
||||
"manualSteps": Array [
|
||||
"Identify the origin of these API calls.",
|
||||
"Update the requests to use the following new API instead: \\"POST /api/new_path\\".",
|
||||
"Check that you are no longer using the old API in any requests, and mark this issue as resolved. It will no longer appear in the Upgrade Assistant unless another call using this API is detected.",
|
||||
],
|
||||
"mark_as_resolved_api": Object {
|
||||
"apiTotalCalls": 13,
|
||||
"routeMethod": "get",
|
||||
"routePath": "/api/test_migrated/",
|
||||
"routeVersion": "123",
|
||||
"timestamp": 2024-10-17T12:06:41.224Z,
|
||||
"totalMarkedAsResolved": 1,
|
||||
},
|
||||
},
|
||||
"deprecationType": "api",
|
||||
"documentationUrl": "https://fake-url",
|
||||
"domainId": "core.routes-deprecations",
|
||||
"level": "critical",
|
||||
"message": Array [
|
||||
"The API \\"GET /api/test_migrated/\\" has been called 13 times. The last call was on Sunday, September 1, 2024 6:06 AM -04:00.",
|
||||
"This issue has been marked as resolved on Thursday, October 17, 2024 8:06 AM -04:00 but the API has been called 12 times since.",
|
||||
],
|
||||
"title": "The \\"GET /api/test_migrated/\\" route is migrated to a different API",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('returns bumped type deprecated route', async () => {
|
||||
const getDeprecations = createGetApiDeprecations({ coreUsageData, http });
|
||||
const deprecatedRoute = createDeprecatedRouteDetails({
|
||||
routePath: '/api/test_bumped/',
|
||||
routeDeprecationOptions: { reason: { type: 'bump', newApiVersion: '444' } },
|
||||
});
|
||||
http.getRegisteredDeprecatedApis.mockReturnValue([deprecatedRoute]);
|
||||
usageClientMock.getDeprecatedApiUsageStats.mockResolvedValue([
|
||||
createApiUsageStat(buildApiDeprecationId(deprecatedRoute)),
|
||||
]);
|
||||
|
||||
const deprecations = await getDeprecations();
|
||||
expect(deprecations).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"apiId": "123|get|/api/test_bumped",
|
||||
"correctiveActions": Object {
|
||||
"manualSteps": Array [
|
||||
"Identify the origin of these API calls.",
|
||||
"Update the requests to use the following new version of the API instead: \\"444\\".",
|
||||
"Check that you are no longer using the old API in any requests, and mark this issue as resolved. It will no longer appear in the Upgrade Assistant unless another call using this API is detected.",
|
||||
],
|
||||
"mark_as_resolved_api": Object {
|
||||
"apiTotalCalls": 13,
|
||||
"routeMethod": "get",
|
||||
"routePath": "/api/test_bumped/",
|
||||
"routeVersion": "123",
|
||||
"timestamp": 2024-10-17T12:06:41.224Z,
|
||||
"totalMarkedAsResolved": 1,
|
||||
},
|
||||
},
|
||||
"deprecationType": "api",
|
||||
"documentationUrl": "https://fake-url",
|
||||
"domainId": "core.routes-deprecations",
|
||||
"level": "critical",
|
||||
"message": Array [
|
||||
"The API \\"GET /api/test_bumped/\\" has been called 13 times. The last call was on Sunday, September 1, 2024 6:06 AM -04:00.",
|
||||
"This issue has been marked as resolved on Thursday, October 17, 2024 8:06 AM -04:00 but the API has been called 12 times since.",
|
||||
],
|
||||
"title": "The \\"GET /api/test_bumped/\\" route has a newer version available",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('does not return resolved deprecated route', async () => {
|
||||
const getDeprecations = createGetApiDeprecations({ coreUsageData, http });
|
||||
const deprecatedRoute = createDeprecatedRouteDetails({ routePath: '/api/test_resolved/' });
|
||||
http.getRegisteredDeprecatedApis.mockReturnValue([deprecatedRoute]);
|
||||
usageClientMock.getDeprecatedApiUsageStats.mockResolvedValue([
|
||||
createApiUsageStat(buildApiDeprecationId(deprecatedRoute), {
|
||||
apiTotalCalls: 5,
|
||||
totalMarkedAsResolved: 5,
|
||||
}),
|
||||
]);
|
||||
|
||||
const deprecations = await getDeprecations();
|
||||
expect(deprecations).toEqual([]);
|
||||
});
|
||||
|
||||
it('returns never resolved deprecated route', async () => {
|
||||
const getDeprecations = createGetApiDeprecations({ coreUsageData, http });
|
||||
const deprecatedRoute = createDeprecatedRouteDetails({
|
||||
routePath: '/api/test_never_resolved/',
|
||||
});
|
||||
http.getRegisteredDeprecatedApis.mockReturnValue([deprecatedRoute]);
|
||||
usageClientMock.getDeprecatedApiUsageStats.mockResolvedValue([
|
||||
createApiUsageStat(buildApiDeprecationId(deprecatedRoute), {
|
||||
totalMarkedAsResolved: 0,
|
||||
markedAsResolvedLastCalledAt: undefined,
|
||||
}),
|
||||
]);
|
||||
|
||||
const deprecations = await getDeprecations();
|
||||
expect(deprecations).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"apiId": "123|get|/api/test_never_resolved",
|
||||
"correctiveActions": Object {
|
||||
"manualSteps": Array [
|
||||
"Identify the origin of these API calls.",
|
||||
"This API no longer exists and no replacement is available. Delete any requests you have that use this API.",
|
||||
"Check that you are no longer using the old API in any requests, and mark this issue as resolved. It will no longer appear in the Upgrade Assistant unless another call using this API is detected.",
|
||||
],
|
||||
"mark_as_resolved_api": Object {
|
||||
"apiTotalCalls": 13,
|
||||
"routeMethod": "get",
|
||||
"routePath": "/api/test_never_resolved/",
|
||||
"routeVersion": "123",
|
||||
"timestamp": 2024-10-17T12:06:41.224Z,
|
||||
"totalMarkedAsResolved": 0,
|
||||
},
|
||||
},
|
||||
"deprecationType": "api",
|
||||
"documentationUrl": "https://fake-url",
|
||||
"domainId": "core.routes-deprecations",
|
||||
"level": "critical",
|
||||
"message": Array [
|
||||
"The API \\"GET /api/test_never_resolved/\\" has been called 13 times. The last call was on Sunday, September 1, 2024 6:06 AM -04:00.",
|
||||
],
|
||||
"title": "The \\"GET /api/test_never_resolved/\\" route is removed",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('does not return deprecated routes that have never been called', async () => {
|
||||
const getDeprecations = createGetApiDeprecations({ coreUsageData, http });
|
||||
const deprecatedRoute = createDeprecatedRouteDetails({
|
||||
routePath: '/api/test_never_resolved/',
|
||||
});
|
||||
http.getRegisteredDeprecatedApis.mockReturnValue([deprecatedRoute]);
|
||||
usageClientMock.getDeprecatedApiUsageStats.mockResolvedValue([]);
|
||||
expect(await getDeprecations()).toEqual([]);
|
||||
|
||||
usageClientMock.getDeprecatedApiUsageStats.mockResolvedValue([
|
||||
createApiUsageStat(buildApiDeprecationId(deprecatedRoute), {
|
||||
apiTotalCalls: 0,
|
||||
apiLastCalledAt: undefined,
|
||||
totalMarkedAsResolved: 0,
|
||||
markedAsResolvedLastCalledAt: undefined,
|
||||
}),
|
||||
]);
|
||||
expect(await getDeprecations()).toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#buildApiDeprecationId', () => {
|
||||
it('returns apiDeprecationId string for versioned routes', () => {
|
||||
const apiDeprecationId = buildApiDeprecationId({
|
||||
routeMethod: 'get',
|
||||
routePath: '/api/test',
|
||||
routeVersion: '10-10-2023',
|
||||
});
|
||||
expect(apiDeprecationId).toBe('10-10-2023|get|/api/test');
|
||||
});
|
||||
|
||||
it('returns apiDeprecationId string for unversioned routes', () => {
|
||||
const apiDeprecationId = buildApiDeprecationId({
|
||||
routeMethod: 'get',
|
||||
routePath: '/api/test',
|
||||
});
|
||||
expect(apiDeprecationId).toBe('unversioned|get|/api/test');
|
||||
});
|
||||
|
||||
it('gives the same ID the route method is capitalized or not', () => {
|
||||
const apiDeprecationId = buildApiDeprecationId({
|
||||
// @ts-expect-error
|
||||
routeMethod: 'GeT',
|
||||
routePath: '/api/test',
|
||||
routeVersion: '10-10-2023',
|
||||
});
|
||||
|
||||
expect(apiDeprecationId).toBe('10-10-2023|get|/api/test');
|
||||
});
|
||||
|
||||
it('gives the same ID the route path has a trailing slash or not', () => {
|
||||
const apiDeprecationId = buildApiDeprecationId({
|
||||
// @ts-expect-error
|
||||
routeMethod: 'GeT',
|
||||
routePath: '/api/test/',
|
||||
routeVersion: '10-10-2023',
|
||||
});
|
||||
|
||||
expect(apiDeprecationId).toBe('10-10-2023|get|/api/test');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,96 @@
|
|||
/*
|
||||
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import type { InternalHttpServiceSetup } from '@kbn/core-http-server-internal';
|
||||
import type { InternalCoreUsageDataSetup } from '@kbn/core-usage-data-base-server-internal';
|
||||
import type { RouterDeprecatedRouteDetails } from '@kbn/core-http-server';
|
||||
import { DeprecationsDetails } from '@kbn/core-deprecations-common';
|
||||
import type { DeprecationsFactory } from '../deprecations_factory';
|
||||
import {
|
||||
getApiDeprecationMessage,
|
||||
getApiDeprecationsManualSteps,
|
||||
getApiDeprecationTitle,
|
||||
} from './i18n_texts';
|
||||
|
||||
interface ApiDeprecationsServiceDeps {
|
||||
deprecationsFactory: DeprecationsFactory;
|
||||
http: InternalHttpServiceSetup;
|
||||
coreUsageData: InternalCoreUsageDataSetup;
|
||||
}
|
||||
|
||||
export const buildApiDeprecationId = ({
|
||||
routePath,
|
||||
routeMethod,
|
||||
routeVersion,
|
||||
}: Pick<RouterDeprecatedRouteDetails, 'routeMethod' | 'routePath' | 'routeVersion'>): string => {
|
||||
return [
|
||||
routeVersion || 'unversioned',
|
||||
routeMethod.toLocaleLowerCase(),
|
||||
routePath.replace(/\/$/, ''),
|
||||
].join('|');
|
||||
};
|
||||
|
||||
export const createGetApiDeprecations =
|
||||
({ http, coreUsageData }: Pick<ApiDeprecationsServiceDeps, 'coreUsageData' | 'http'>) =>
|
||||
async (): Promise<DeprecationsDetails[]> => {
|
||||
const deprecatedRoutes = http.getRegisteredDeprecatedApis();
|
||||
const usageClient = coreUsageData.getClient();
|
||||
const deprecatedApiUsageStats = await usageClient.getDeprecatedApiUsageStats();
|
||||
|
||||
return deprecatedApiUsageStats
|
||||
.filter(({ apiTotalCalls, totalMarkedAsResolved }) => {
|
||||
return apiTotalCalls > totalMarkedAsResolved;
|
||||
})
|
||||
.filter(({ apiId }) =>
|
||||
deprecatedRoutes.some((routeDetails) => buildApiDeprecationId(routeDetails) === apiId)
|
||||
)
|
||||
.map((apiUsageStats) => {
|
||||
const { apiId, apiTotalCalls, totalMarkedAsResolved } = apiUsageStats;
|
||||
const routeDeprecationDetails = deprecatedRoutes.find(
|
||||
(routeDetails) => buildApiDeprecationId(routeDetails) === apiId
|
||||
)!;
|
||||
const { routeVersion, routePath, routeDeprecationOptions, routeMethod } =
|
||||
routeDeprecationDetails;
|
||||
|
||||
const deprecationLevel = routeDeprecationOptions.severity || 'warning';
|
||||
|
||||
return {
|
||||
apiId,
|
||||
title: getApiDeprecationTitle(routeDeprecationDetails),
|
||||
level: deprecationLevel,
|
||||
message: getApiDeprecationMessage(routeDeprecationDetails, apiUsageStats),
|
||||
documentationUrl: routeDeprecationOptions.documentationUrl,
|
||||
correctiveActions: {
|
||||
manualSteps: getApiDeprecationsManualSteps(routeDeprecationDetails),
|
||||
mark_as_resolved_api: {
|
||||
routePath,
|
||||
routeMethod,
|
||||
routeVersion,
|
||||
apiTotalCalls,
|
||||
totalMarkedAsResolved,
|
||||
timestamp: new Date(),
|
||||
},
|
||||
},
|
||||
deprecationType: 'api',
|
||||
domainId: 'core.routes-deprecations',
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
export const registerApiDeprecationsInfo = ({
|
||||
deprecationsFactory,
|
||||
http,
|
||||
coreUsageData,
|
||||
}: ApiDeprecationsServiceDeps): void => {
|
||||
const deprecationsRegistery = deprecationsFactory.getRegistry('core.api_deprecations');
|
||||
|
||||
deprecationsRegistery.registerDeprecations({
|
||||
getDeprecations: createGetApiDeprecations({ http, coreUsageData }),
|
||||
});
|
||||
};
|
|
@ -0,0 +1,115 @@
|
|||
/*
|
||||
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { registerConfigDeprecationsInfo } from './config_deprecations';
|
||||
import { mockDeprecationsRegistry, mockDeprecationsFactory } from '../mocks';
|
||||
import { mockCoreContext } from '@kbn/core-base-server-mocks';
|
||||
import { configServiceMock } from '@kbn/config-mocks';
|
||||
|
||||
describe('#registerConfigDeprecationsInfo', () => {
|
||||
let coreContext: ReturnType<typeof mockCoreContext.create>;
|
||||
|
||||
const deprecationsFactory = mockDeprecationsFactory.create();
|
||||
const deprecationsRegistry = mockDeprecationsRegistry.create();
|
||||
const getDeprecationsContext = mockDeprecationsRegistry.createGetDeprecationsContext();
|
||||
|
||||
beforeEach(() => {
|
||||
const configService = configServiceMock.create({
|
||||
atPath: { skip_deprecated_settings: ['hello', 'world'] },
|
||||
});
|
||||
jest.clearAllMocks();
|
||||
coreContext = mockCoreContext.create({ configService });
|
||||
});
|
||||
|
||||
it('registers config deprecations', async () => {
|
||||
coreContext.configService.getHandledDeprecatedConfigs.mockReturnValue([
|
||||
[
|
||||
'testDomain',
|
||||
[
|
||||
{
|
||||
configPath: 'test',
|
||||
level: 'critical',
|
||||
message: 'testMessage',
|
||||
documentationUrl: 'testDocUrl',
|
||||
correctiveActions: {
|
||||
manualSteps: [
|
||||
'Using Kibana user management, change all users using the kibana_user role to the kibana_admin role.',
|
||||
'Using Kibana role-mapping management, change all role-mappings which assign the kibana_user role to the kibana_admin role.',
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
deprecationsFactory.getRegistry.mockReturnValue(deprecationsRegistry);
|
||||
registerConfigDeprecationsInfo({
|
||||
deprecationsFactory,
|
||||
configService: coreContext.configService,
|
||||
});
|
||||
|
||||
expect(coreContext.configService.getHandledDeprecatedConfigs).toBeCalledTimes(1);
|
||||
expect(deprecationsFactory.getRegistry).toBeCalledTimes(1);
|
||||
expect(deprecationsFactory.getRegistry).toBeCalledWith('testDomain');
|
||||
expect(deprecationsRegistry.registerDeprecations).toBeCalledTimes(1);
|
||||
const configDeprecations =
|
||||
await deprecationsRegistry.registerDeprecations.mock.calls[0][0].getDeprecations(
|
||||
getDeprecationsContext
|
||||
);
|
||||
expect(configDeprecations).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"configPath": "test",
|
||||
"correctiveActions": Object {
|
||||
"manualSteps": Array [
|
||||
"Using Kibana user management, change all users using the kibana_user role to the kibana_admin role.",
|
||||
"Using Kibana role-mapping management, change all role-mappings which assign the kibana_user role to the kibana_admin role.",
|
||||
],
|
||||
},
|
||||
"deprecationType": "config",
|
||||
"documentationUrl": "testDocUrl",
|
||||
"level": "critical",
|
||||
"message": "testMessage",
|
||||
"requireRestart": true,
|
||||
"title": "testDomain has a deprecated setting",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('accepts `level` field overrides', async () => {
|
||||
coreContext.configService.getHandledDeprecatedConfigs.mockReturnValue([
|
||||
[
|
||||
'testDomain',
|
||||
[
|
||||
{
|
||||
configPath: 'test',
|
||||
message: 'testMessage',
|
||||
level: 'warning',
|
||||
correctiveActions: {
|
||||
manualSteps: ['step a'],
|
||||
},
|
||||
},
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
deprecationsFactory.getRegistry.mockReturnValue(deprecationsRegistry);
|
||||
registerConfigDeprecationsInfo({
|
||||
deprecationsFactory,
|
||||
configService: coreContext.configService,
|
||||
});
|
||||
|
||||
const configDeprecations =
|
||||
await deprecationsRegistry.registerDeprecations.mock.calls[0][0].getDeprecations(
|
||||
getDeprecationsContext
|
||||
);
|
||||
expect(configDeprecations[0].level).toBe('warning');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import type { IConfigService } from '@kbn/config';
|
||||
import { DeprecationsFactory } from '../deprecations_factory';
|
||||
|
||||
interface RegisterConfigDeprecationsInfo {
|
||||
deprecationsFactory: DeprecationsFactory;
|
||||
configService: IConfigService;
|
||||
}
|
||||
|
||||
export const registerConfigDeprecationsInfo = ({
|
||||
deprecationsFactory,
|
||||
configService,
|
||||
}: RegisterConfigDeprecationsInfo) => {
|
||||
const handledDeprecatedConfigs = configService.getHandledDeprecatedConfigs();
|
||||
|
||||
for (const [domainId, deprecationsContexts] of handledDeprecatedConfigs) {
|
||||
const deprecationsRegistry = deprecationsFactory.getRegistry(domainId);
|
||||
deprecationsRegistry.registerDeprecations({
|
||||
getDeprecations: () => {
|
||||
return deprecationsContexts.map(
|
||||
({
|
||||
configPath,
|
||||
title = `${domainId} has a deprecated setting`,
|
||||
level,
|
||||
message,
|
||||
correctiveActions,
|
||||
documentationUrl,
|
||||
}) => ({
|
||||
configPath,
|
||||
title,
|
||||
level,
|
||||
message,
|
||||
correctiveActions,
|
||||
documentationUrl,
|
||||
deprecationType: 'config',
|
||||
requireRestart: true,
|
||||
})
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
|
@ -0,0 +1,132 @@
|
|||
/*
|
||||
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { RouterDeprecatedRouteDetails } from '@kbn/core-http-server';
|
||||
import { CoreDeprecatedApiUsageStats } from '@kbn/core-usage-data-server';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import moment from 'moment';
|
||||
|
||||
export const getApiDeprecationTitle = (details: RouterDeprecatedRouteDetails) => {
|
||||
const { routePath, routeMethod, routeDeprecationOptions } = details;
|
||||
const deprecationType = routeDeprecationOptions.reason.type;
|
||||
const routeWithMethod = `${routeMethod.toUpperCase()} ${routePath}`;
|
||||
const deprecationTypeText = i18n.translate('core.deprecations.deprecations.apiDeprecationType', {
|
||||
defaultMessage:
|
||||
'{deprecationType, select, remove {is removed} bump {has a newer version available} migrate {is migrated to a different API} other {is deprecated}}',
|
||||
values: { deprecationType },
|
||||
});
|
||||
|
||||
return i18n.translate('core.deprecations.deprecations.apiDeprecationInfoTitle', {
|
||||
defaultMessage: 'The "{routeWithMethod}" route {deprecationTypeText}',
|
||||
values: {
|
||||
routeWithMethod,
|
||||
deprecationTypeText,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const getApiDeprecationMessage = (
|
||||
details: RouterDeprecatedRouteDetails,
|
||||
apiUsageStats: CoreDeprecatedApiUsageStats
|
||||
): string[] => {
|
||||
const { routePath, routeMethod } = details;
|
||||
const { apiLastCalledAt, apiTotalCalls, markedAsResolvedLastCalledAt, totalMarkedAsResolved } =
|
||||
apiUsageStats;
|
||||
|
||||
const diff = apiTotalCalls - totalMarkedAsResolved;
|
||||
const wasResolvedBefore = totalMarkedAsResolved > 0;
|
||||
const routeWithMethod = `${routeMethod.toUpperCase()} ${routePath}`;
|
||||
|
||||
const messages = [
|
||||
i18n.translate('core.deprecations.deprecations.apiDeprecationApiCallsDetailsMessage', {
|
||||
defaultMessage:
|
||||
'The API "{routeWithMethod}" has been called {apiTotalCalls} times. The last call was on {apiLastCalledAt}.',
|
||||
values: {
|
||||
routeWithMethod,
|
||||
apiTotalCalls,
|
||||
apiLastCalledAt: moment(apiLastCalledAt).format('LLLL Z'),
|
||||
},
|
||||
}),
|
||||
];
|
||||
|
||||
if (wasResolvedBefore) {
|
||||
messages.push(
|
||||
i18n.translate(
|
||||
'core.deprecations.deprecations.apiDeprecationPreviouslyMarkedAsResolvedMessage',
|
||||
{
|
||||
defaultMessage:
|
||||
'This issue has been marked as resolved on {markedAsResolvedLastCalledAt} but the API has been called {timeSinceLastResolved, plural, one {# time} other {# times}} since.',
|
||||
values: {
|
||||
timeSinceLastResolved: diff,
|
||||
markedAsResolvedLastCalledAt: moment(markedAsResolvedLastCalledAt).format('LLLL Z'),
|
||||
},
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return messages;
|
||||
};
|
||||
|
||||
export const getApiDeprecationsManualSteps = (details: RouterDeprecatedRouteDetails): string[] => {
|
||||
const { routeDeprecationOptions } = details;
|
||||
const deprecationType = routeDeprecationOptions.reason.type;
|
||||
|
||||
const manualSteps = [
|
||||
i18n.translate('core.deprecations.deprecations.manualSteps.apiIseprecatedStep', {
|
||||
defaultMessage: 'Identify the origin of these API calls.',
|
||||
}),
|
||||
];
|
||||
|
||||
switch (deprecationType) {
|
||||
case 'bump': {
|
||||
const { newApiVersion } = routeDeprecationOptions.reason;
|
||||
manualSteps.push(
|
||||
i18n.translate('core.deprecations.deprecations.manualSteps.bumpDetailsStep', {
|
||||
defaultMessage:
|
||||
'Update the requests to use the following new version of the API instead: "{newApiVersion}".',
|
||||
values: { newApiVersion },
|
||||
})
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'remove': {
|
||||
manualSteps.push(
|
||||
i18n.translate('core.deprecations.deprecations.manualSteps.removeTypeExplainationStep', {
|
||||
defaultMessage:
|
||||
'This API no longer exists and no replacement is available. Delete any requests you have that use this API.',
|
||||
})
|
||||
);
|
||||
break;
|
||||
}
|
||||
case 'migrate': {
|
||||
const { newApiPath, newApiMethod } = routeDeprecationOptions.reason;
|
||||
const newRouteWithMethod = `${newApiMethod.toUpperCase()} ${newApiPath}`;
|
||||
|
||||
manualSteps.push(
|
||||
i18n.translate('core.deprecations.deprecations.manualSteps.migrateDetailsStep', {
|
||||
defaultMessage:
|
||||
'Update the requests to use the following new API instead: "{newRouteWithMethod}".',
|
||||
values: { newRouteWithMethod },
|
||||
})
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
manualSteps.push(
|
||||
i18n.translate('core.deprecations.deprecations.manualSteps.markAsResolvedStep', {
|
||||
defaultMessage:
|
||||
'Check that you are no longer using the old API in any requests, and mark this issue as resolved. It will no longer appear in the Upgrade Assistant unless another call using this API is detected.',
|
||||
})
|
||||
);
|
||||
|
||||
return manualSteps;
|
||||
};
|
|
@ -0,0 +1,11 @@
|
|||
/*
|
||||
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
export { buildApiDeprecationId, registerApiDeprecationsInfo } from './api_deprecations';
|
||||
export { registerConfigDeprecationsInfo } from './config_deprecations';
|
|
@ -14,6 +14,14 @@ export const DeprecationsFactoryMock = jest
|
|||
.fn()
|
||||
.mockImplementation(() => mockedDeprecationFactoryInstance);
|
||||
|
||||
export const registerConfigDeprecationsInfoMock = jest.fn();
|
||||
export const registerApiDeprecationsInfoMock = jest.fn();
|
||||
|
||||
jest.doMock('./deprecations', () => ({
|
||||
registerConfigDeprecationsInfo: registerConfigDeprecationsInfoMock,
|
||||
registerApiDeprecationsInfo: registerApiDeprecationsInfoMock,
|
||||
}));
|
||||
|
||||
jest.doMock('./deprecations_factory', () => ({
|
||||
DeprecationsFactory: DeprecationsFactoryMock,
|
||||
}));
|
||||
|
|
|
@ -7,22 +7,24 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { DeprecationsFactoryMock } from './deprecations_service.test.mocks';
|
||||
|
||||
import {
|
||||
DeprecationsFactoryMock,
|
||||
registerConfigDeprecationsInfoMock,
|
||||
} from './deprecations_service.test.mocks';
|
||||
import { mockCoreContext } from '@kbn/core-base-server-mocks';
|
||||
import { httpServiceMock } from '@kbn/core-http-server-mocks';
|
||||
import { coreUsageDataServiceMock } from '@kbn/core-usage-data-server-mocks';
|
||||
import { configServiceMock } from '@kbn/config-mocks';
|
||||
import { savedObjectsClientMock } from '@kbn/core-saved-objects-api-server-mocks';
|
||||
import { elasticsearchServiceMock } from '@kbn/core-elasticsearch-server-mocks';
|
||||
import { DeprecationsService, DeprecationsSetupDeps } from './deprecations_service';
|
||||
import { mockDeprecationsRegistry, mockDeprecationsFactory } from './mocks';
|
||||
|
||||
/* eslint-disable dot-notation */
|
||||
describe('DeprecationsService', () => {
|
||||
let coreContext: ReturnType<typeof mockCoreContext.create>;
|
||||
let http: ReturnType<typeof httpServiceMock.createInternalSetupContract>;
|
||||
let router: ReturnType<typeof httpServiceMock.createRouter>;
|
||||
let deprecationsCoreSetupDeps: DeprecationsSetupDeps;
|
||||
let coreUsageData: ReturnType<typeof coreUsageDataServiceMock.createSetupContract>;
|
||||
|
||||
beforeEach(() => {
|
||||
const configService = configServiceMock.create({
|
||||
|
@ -30,14 +32,16 @@ describe('DeprecationsService', () => {
|
|||
});
|
||||
coreContext = mockCoreContext.create({ configService });
|
||||
http = httpServiceMock.createInternalSetupContract();
|
||||
coreUsageData = coreUsageDataServiceMock.createSetupContract();
|
||||
router = httpServiceMock.createRouter();
|
||||
http.createRouter.mockReturnValue(router);
|
||||
deprecationsCoreSetupDeps = { http };
|
||||
deprecationsCoreSetupDeps = { http, coreUsageData };
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
DeprecationsFactoryMock.mockClear();
|
||||
registerConfigDeprecationsInfoMock.mockClear();
|
||||
});
|
||||
|
||||
describe('#setup', () => {
|
||||
|
@ -53,10 +57,8 @@ describe('DeprecationsService', () => {
|
|||
|
||||
it('calls registerConfigDeprecationsInfo', async () => {
|
||||
const deprecationsService = new DeprecationsService(coreContext);
|
||||
const mockRegisterConfigDeprecationsInfo = jest.fn();
|
||||
deprecationsService['registerConfigDeprecationsInfo'] = mockRegisterConfigDeprecationsInfo;
|
||||
await deprecationsService.setup(deprecationsCoreSetupDeps);
|
||||
expect(mockRegisterConfigDeprecationsInfo).toBeCalledTimes(1);
|
||||
expect(registerConfigDeprecationsInfoMock).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
it('creates DeprecationsFactory with the correct parameters', async () => {
|
||||
|
@ -89,92 +91,4 @@ describe('DeprecationsService', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#registerConfigDeprecationsInfo', () => {
|
||||
const deprecationsFactory = mockDeprecationsFactory.create();
|
||||
const deprecationsRegistry = mockDeprecationsRegistry.create();
|
||||
const getDeprecationsContext = mockDeprecationsRegistry.createGetDeprecationsContext();
|
||||
|
||||
it('registers config deprecations', async () => {
|
||||
const deprecationsService = new DeprecationsService(coreContext);
|
||||
coreContext.configService.getHandledDeprecatedConfigs.mockReturnValue([
|
||||
[
|
||||
'testDomain',
|
||||
[
|
||||
{
|
||||
configPath: 'test',
|
||||
level: 'critical',
|
||||
message: 'testMessage',
|
||||
documentationUrl: 'testDocUrl',
|
||||
correctiveActions: {
|
||||
manualSteps: [
|
||||
'Using Kibana user management, change all users using the kibana_user role to the kibana_admin role.',
|
||||
'Using Kibana role-mapping management, change all role-mappings which assign the kibana_user role to the kibana_admin role.',
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
deprecationsFactory.getRegistry.mockReturnValue(deprecationsRegistry);
|
||||
deprecationsService['registerConfigDeprecationsInfo'](deprecationsFactory);
|
||||
|
||||
expect(coreContext.configService.getHandledDeprecatedConfigs).toBeCalledTimes(1);
|
||||
expect(deprecationsFactory.getRegistry).toBeCalledTimes(1);
|
||||
expect(deprecationsFactory.getRegistry).toBeCalledWith('testDomain');
|
||||
expect(deprecationsRegistry.registerDeprecations).toBeCalledTimes(1);
|
||||
const configDeprecations =
|
||||
await deprecationsRegistry.registerDeprecations.mock.calls[0][0].getDeprecations(
|
||||
getDeprecationsContext
|
||||
);
|
||||
expect(configDeprecations).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"configPath": "test",
|
||||
"correctiveActions": Object {
|
||||
"manualSteps": Array [
|
||||
"Using Kibana user management, change all users using the kibana_user role to the kibana_admin role.",
|
||||
"Using Kibana role-mapping management, change all role-mappings which assign the kibana_user role to the kibana_admin role.",
|
||||
],
|
||||
},
|
||||
"deprecationType": "config",
|
||||
"documentationUrl": "testDocUrl",
|
||||
"level": "critical",
|
||||
"message": "testMessage",
|
||||
"requireRestart": true,
|
||||
"title": "testDomain has a deprecated setting",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('accepts `level` field overrides', async () => {
|
||||
const deprecationsService = new DeprecationsService(coreContext);
|
||||
coreContext.configService.getHandledDeprecatedConfigs.mockReturnValue([
|
||||
[
|
||||
'testDomain',
|
||||
[
|
||||
{
|
||||
configPath: 'test',
|
||||
message: 'testMessage',
|
||||
level: 'warning',
|
||||
correctiveActions: {
|
||||
manualSteps: ['step a'],
|
||||
},
|
||||
},
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
deprecationsFactory.getRegistry.mockReturnValue(deprecationsRegistry);
|
||||
deprecationsService['registerConfigDeprecationsInfo'](deprecationsFactory);
|
||||
|
||||
const configDeprecations =
|
||||
await deprecationsRegistry.registerDeprecations.mock.calls[0][0].getDeprecations(
|
||||
getDeprecationsContext
|
||||
);
|
||||
expect(configDeprecations[0].level).toBe('warning');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -19,9 +19,11 @@ import type {
|
|||
DeprecationRegistryProvider,
|
||||
DeprecationsClient,
|
||||
} from '@kbn/core-deprecations-server';
|
||||
import { InternalCoreUsageDataSetup } from '@kbn/core-usage-data-base-server-internal';
|
||||
import { DeprecationsFactory } from './deprecations_factory';
|
||||
import { registerRoutes } from './routes';
|
||||
import { config as deprecationConfig, DeprecationConfigType } from './deprecation_config';
|
||||
import { registerApiDeprecationsInfo, registerConfigDeprecationsInfo } from './deprecations';
|
||||
|
||||
export interface InternalDeprecationsServiceStart {
|
||||
/**
|
||||
|
@ -40,6 +42,7 @@ export type InternalDeprecationsServiceSetup = DeprecationRegistryProvider;
|
|||
/** @internal */
|
||||
export interface DeprecationsSetupDeps {
|
||||
http: InternalHttpServiceSetup;
|
||||
coreUsageData: InternalCoreUsageDataSetup;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
|
@ -55,7 +58,10 @@ export class DeprecationsService
|
|||
this.configService = coreContext.configService;
|
||||
}
|
||||
|
||||
public async setup({ http }: DeprecationsSetupDeps): Promise<InternalDeprecationsServiceSetup> {
|
||||
public async setup({
|
||||
http,
|
||||
coreUsageData,
|
||||
}: DeprecationsSetupDeps): Promise<InternalDeprecationsServiceSetup> {
|
||||
this.logger.debug('Setting up Deprecations service');
|
||||
|
||||
const config = await firstValueFrom(
|
||||
|
@ -69,8 +75,18 @@ export class DeprecationsService
|
|||
},
|
||||
});
|
||||
|
||||
registerRoutes({ http });
|
||||
this.registerConfigDeprecationsInfo(this.deprecationsFactory);
|
||||
registerRoutes({ http, coreUsageData });
|
||||
|
||||
registerConfigDeprecationsInfo({
|
||||
deprecationsFactory: this.deprecationsFactory,
|
||||
configService: this.configService,
|
||||
});
|
||||
|
||||
registerApiDeprecationsInfo({
|
||||
deprecationsFactory: this.deprecationsFactory,
|
||||
http,
|
||||
coreUsageData,
|
||||
});
|
||||
|
||||
const deprecationsFactory = this.deprecationsFactory;
|
||||
return {
|
||||
|
@ -87,6 +103,7 @@ export class DeprecationsService
|
|||
if (!this.deprecationsFactory) {
|
||||
throw new Error('`setup` must be called before `start`');
|
||||
}
|
||||
|
||||
return {
|
||||
asScopedToClient: this.createScopedDeprecations(),
|
||||
};
|
||||
|
@ -107,35 +124,4 @@ export class DeprecationsService
|
|||
};
|
||||
};
|
||||
}
|
||||
|
||||
private registerConfigDeprecationsInfo(deprecationsFactory: DeprecationsFactory) {
|
||||
const handledDeprecatedConfigs = this.configService.getHandledDeprecatedConfigs();
|
||||
|
||||
for (const [domainId, deprecationsContexts] of handledDeprecatedConfigs) {
|
||||
const deprecationsRegistry = deprecationsFactory.getRegistry(domainId);
|
||||
deprecationsRegistry.registerDeprecations({
|
||||
getDeprecations: () => {
|
||||
return deprecationsContexts.map(
|
||||
({
|
||||
configPath,
|
||||
title = `${domainId} has a deprecated setting`,
|
||||
level,
|
||||
message,
|
||||
correctiveActions,
|
||||
documentationUrl,
|
||||
}) => ({
|
||||
configPath,
|
||||
title,
|
||||
level,
|
||||
message,
|
||||
correctiveActions,
|
||||
documentationUrl,
|
||||
deprecationType: 'config',
|
||||
requireRestart: true,
|
||||
})
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,10 +8,22 @@
|
|||
*/
|
||||
|
||||
import type { InternalHttpServiceSetup } from '@kbn/core-http-server-internal';
|
||||
import { InternalCoreUsageDataSetup } from '@kbn/core-usage-data-base-server-internal';
|
||||
import type { InternalDeprecationRequestHandlerContext } from '../internal_types';
|
||||
import { registerGetRoute } from './get';
|
||||
import { registerMarkAsResolvedRoute } from './resolve_deprecated_api';
|
||||
import { registerApiDeprecationsPostValidationHandler } from './post_validation_handler';
|
||||
|
||||
export function registerRoutes({ http }: { http: InternalHttpServiceSetup }) {
|
||||
export function registerRoutes({
|
||||
http,
|
||||
coreUsageData,
|
||||
}: {
|
||||
http: InternalHttpServiceSetup;
|
||||
coreUsageData: InternalCoreUsageDataSetup;
|
||||
}) {
|
||||
const router = http.createRouter<InternalDeprecationRequestHandlerContext>('/api/deprecations');
|
||||
registerGetRoute(router);
|
||||
|
||||
registerApiDeprecationsPostValidationHandler({ http, coreUsageData });
|
||||
registerMarkAsResolvedRoute(router, { coreUsageData });
|
||||
}
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import type { InternalCoreUsageDataSetup } from '@kbn/core-usage-data-server-internal';
|
||||
import type { CoreKibanaRequest } from '@kbn/core-http-router-server-internal';
|
||||
import type { InternalHttpServiceSetup } from '@kbn/core-http-server-internal';
|
||||
import { isObject } from 'lodash';
|
||||
import { RouteDeprecationInfo } from '@kbn/core-http-server/src/router/route';
|
||||
import { buildApiDeprecationId } from '../deprecations';
|
||||
|
||||
interface Dependencies {
|
||||
coreUsageData: InternalCoreUsageDataSetup;
|
||||
http: InternalHttpServiceSetup;
|
||||
}
|
||||
|
||||
/**
|
||||
* listens to http post validation events to increment deprecated api calls
|
||||
* This will keep track of any called deprecated API.
|
||||
*/
|
||||
export const registerApiDeprecationsPostValidationHandler = ({
|
||||
coreUsageData,
|
||||
http,
|
||||
}: Dependencies) => {
|
||||
http.registerOnPostValidation(createRouteDeprecationsHandler({ coreUsageData }));
|
||||
};
|
||||
|
||||
export function createRouteDeprecationsHandler({
|
||||
coreUsageData,
|
||||
}: {
|
||||
coreUsageData: InternalCoreUsageDataSetup;
|
||||
}) {
|
||||
return (req: CoreKibanaRequest, { deprecated }: { deprecated?: RouteDeprecationInfo }) => {
|
||||
if (deprecated && isObject(deprecated) && req.route.routePath) {
|
||||
const counterName = buildApiDeprecationId({
|
||||
routeMethod: req.route.method,
|
||||
routePath: req.route.routePath,
|
||||
routeVersion: req.apiVersion,
|
||||
});
|
||||
|
||||
const client = coreUsageData.getClient();
|
||||
// no await we just fire it off.
|
||||
void client.incrementDeprecatedApi(counterName, { resolved: false });
|
||||
}
|
||||
};
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { schema } from '@kbn/config-schema';
|
||||
import { InternalCoreUsageDataSetup } from '@kbn/core-usage-data-base-server-internal';
|
||||
import type { InternalDeprecationRouter } from '../internal_types';
|
||||
import { buildApiDeprecationId } from '../deprecations';
|
||||
|
||||
export const registerMarkAsResolvedRoute = (
|
||||
router: InternalDeprecationRouter,
|
||||
{ coreUsageData }: { coreUsageData: InternalCoreUsageDataSetup }
|
||||
) => {
|
||||
router.post(
|
||||
{
|
||||
path: '/mark_as_resolved',
|
||||
validate: {
|
||||
body: schema.object({
|
||||
domainId: schema.string(),
|
||||
routePath: schema.string(),
|
||||
routeMethod: schema.oneOf([
|
||||
schema.literal('post'),
|
||||
schema.literal('put'),
|
||||
schema.literal('delete'),
|
||||
schema.literal('patch'),
|
||||
schema.literal('get'),
|
||||
schema.literal('options'),
|
||||
]),
|
||||
routeVersion: schema.maybe(schema.string()),
|
||||
incrementBy: schema.number(),
|
||||
}),
|
||||
},
|
||||
},
|
||||
async (_, req, res) => {
|
||||
const usageClient = coreUsageData.getClient();
|
||||
const { routeMethod, routePath, routeVersion, incrementBy } = req.body;
|
||||
const counterName = buildApiDeprecationId({
|
||||
routeMethod,
|
||||
routePath,
|
||||
routeVersion,
|
||||
});
|
||||
|
||||
await usageClient.incrementDeprecatedApi(counterName, { resolved: true, incrementBy });
|
||||
return res.ok();
|
||||
}
|
||||
);
|
||||
};
|
|
@ -33,6 +33,11 @@
|
|||
"@kbn/core-elasticsearch-server-mocks",
|
||||
"@kbn/core-http-server",
|
||||
"@kbn/core-elasticsearch-client-server-mocks",
|
||||
"@kbn/core-usage-data-base-server-internal",
|
||||
"@kbn/core-usage-data-server",
|
||||
"@kbn/core-usage-data-server-internal",
|
||||
"@kbn/core-usage-data-server-mocks",
|
||||
"@kbn/core-http-router-server-internal",
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*",
|
||||
|
|
|
@ -13,16 +13,10 @@ export {
|
|||
CoreVersionedRouter,
|
||||
ALLOWED_PUBLIC_VERSION,
|
||||
unwrapVersionedResponseBodyValidation,
|
||||
type VersionedRouterRoute,
|
||||
type HandlerResolutionStrategy,
|
||||
} from './src/versioned_router';
|
||||
export { Router } from './src/router';
|
||||
export type {
|
||||
RouterOptions,
|
||||
InternalRegistrar,
|
||||
InternalRegistrarOptions,
|
||||
InternalRouterRoute,
|
||||
} from './src/router';
|
||||
export type { RouterOptions, InternalRegistrar, InternalRegistrarOptions } from './src/router';
|
||||
export { isKibanaRequest, isRealRequest, ensureRawRequest, CoreKibanaRequest } from './src/request';
|
||||
export { isSafeMethod } from './src/route';
|
||||
export { HapiResponseAdapter } from './src/response_adapter';
|
||||
|
|
|
@ -143,6 +143,8 @@ export class CoreKibanaRequest<
|
|||
public readonly rewrittenUrl?: URL;
|
||||
/** {@inheritDoc KibanaRequest.httpVersion} */
|
||||
public readonly httpVersion: string;
|
||||
/** {@inheritDoc KibanaRequest.apiVersion} */
|
||||
public readonly apiVersion: undefined;
|
||||
/** {@inheritDoc KibanaRequest.protocol} */
|
||||
public readonly protocol: HttpProtocol;
|
||||
/** {@inheritDoc KibanaRequest.authzResult} */
|
||||
|
@ -185,6 +187,7 @@ export class CoreKibanaRequest<
|
|||
});
|
||||
|
||||
this.httpVersion = isRealReq ? request.raw.req.httpVersion : '1.0';
|
||||
this.apiVersion = undefined;
|
||||
this.protocol = getProtocolFromHttpVersion(this.httpVersion);
|
||||
|
||||
this.route = deepFreeze(this.getRouteInfo(request));
|
||||
|
@ -216,6 +219,7 @@ export class CoreKibanaRequest<
|
|||
},
|
||||
route: this.route,
|
||||
authzResult: this.authzResult,
|
||||
apiVersion: this.apiVersion,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -252,7 +256,14 @@ export class CoreKibanaRequest<
|
|||
} = request.route?.settings?.payload || {};
|
||||
|
||||
// the socket is undefined when using @hapi/shot, or when a "fake request" is used
|
||||
const socketTimeout = isRealRawRequest(request) ? request.raw.req.socket?.timeout : undefined;
|
||||
let socketTimeout: undefined | number;
|
||||
let routePath: undefined | string;
|
||||
|
||||
if (isRealRawRequest(request)) {
|
||||
socketTimeout = request.raw.req.socket?.timeout;
|
||||
routePath = request.route.path;
|
||||
}
|
||||
|
||||
const options = {
|
||||
authRequired: this.getAuthRequired(request),
|
||||
// TypeScript note: Casting to `RouterOptions` to fix the following error:
|
||||
|
@ -266,6 +277,8 @@ export class CoreKibanaRequest<
|
|||
xsrfRequired:
|
||||
((request.route?.settings as RouteOptions)?.app as KibanaRouteOptions)?.xsrfRequired ??
|
||||
true, // some places in LP call KibanaRequest.from(request) manually. remove fallback to true before v8
|
||||
deprecated: ((request.route?.settings as RouteOptions)?.app as KibanaRouteOptions)
|
||||
?.deprecated,
|
||||
access: this.getAccess(request),
|
||||
tags: request.route?.settings?.tags || [],
|
||||
security: this.getSecurity(request),
|
||||
|
@ -285,6 +298,7 @@ export class CoreKibanaRequest<
|
|||
|
||||
return {
|
||||
path: request.path ?? '/',
|
||||
routePath,
|
||||
method,
|
||||
options,
|
||||
};
|
||||
|
|
|
@ -50,7 +50,11 @@ describe('Router', () => {
|
|||
path: '/',
|
||||
validate: { body: validation, query: validation, params: validation },
|
||||
options: {
|
||||
deprecated: true,
|
||||
deprecated: {
|
||||
documentationUrl: 'https://fake-url.com',
|
||||
reason: { type: 'remove' },
|
||||
severity: 'warning',
|
||||
},
|
||||
discontinued: 'post test discontinued',
|
||||
summary: 'post test summary',
|
||||
description: 'post test description',
|
||||
|
@ -72,7 +76,11 @@ describe('Router', () => {
|
|||
validationSchemas: { body: validation, query: validation, params: validation },
|
||||
isVersioned: false,
|
||||
options: {
|
||||
deprecated: true,
|
||||
deprecated: {
|
||||
documentationUrl: 'https://fake-url.com',
|
||||
reason: { type: 'remove' },
|
||||
severity: 'warning',
|
||||
},
|
||||
discontinued: 'post test discontinued',
|
||||
summary: 'post test summary',
|
||||
description: 'post test description',
|
||||
|
@ -93,7 +101,7 @@ describe('Router', () => {
|
|||
validate: { body: validation, query: validation, params: validation },
|
||||
},
|
||||
(context, req, res) => res.ok(),
|
||||
{ isVersioned: true }
|
||||
{ isVersioned: true, events: false }
|
||||
);
|
||||
router.get(
|
||||
{
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { EventEmitter } from 'node:events';
|
||||
import type { Request, ResponseToolkit } from '@hapi/hapi';
|
||||
import apm from 'elastic-apm-node';
|
||||
import { isConfigSchema } from '@kbn/config-schema';
|
||||
|
@ -32,6 +33,7 @@ import { isZod } from '@kbn/zod';
|
|||
import { validBodyOutput, getRequestValidation } from '@kbn/core-http-server';
|
||||
import type { RouteSecurityGetter } from '@kbn/core-http-server';
|
||||
import type { DeepPartial } from '@kbn/utility-types';
|
||||
import { RouteDeprecationInfo } from '@kbn/core-http-server/src/router/route';
|
||||
import { RouteValidator } from './validator';
|
||||
import { ALLOWED_PUBLIC_VERSION, CoreVersionedRouter } from './versioned_router';
|
||||
import { CoreKibanaRequest } from './request';
|
||||
|
@ -52,7 +54,7 @@ export type ContextEnhancer<
|
|||
Context extends RequestHandlerContextBase
|
||||
> = (handler: RequestHandler<P, Q, B, Context, Method>) => RequestHandlerEnhanced<P, Q, B, Method>;
|
||||
|
||||
function getRouteFullPath(routerPath: string, routePath: string) {
|
||||
export function getRouteFullPath(routerPath: string, routePath: string) {
|
||||
// If router's path ends with slash and route's path starts with slash,
|
||||
// we should omit one of them to have a valid concatenated path.
|
||||
const routePathStartIndex = routerPath.endsWith('/') && routePath.startsWith('/') ? 1 : 0;
|
||||
|
@ -147,7 +149,13 @@ export interface RouterOptions {
|
|||
|
||||
/** @internal */
|
||||
export interface InternalRegistrarOptions {
|
||||
/** @default false */
|
||||
isVersioned: boolean;
|
||||
/**
|
||||
* Whether this route should emit "route events" like postValidate
|
||||
* @default true
|
||||
*/
|
||||
events: boolean;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
|
@ -166,15 +174,9 @@ export type InternalRegistrar<M extends Method, C extends RequestHandlerContextB
|
|||
) => ReturnType<RouteRegistrar<M, C>>;
|
||||
|
||||
/** @internal */
|
||||
export interface InternalRouterRoute extends RouterRoute {
|
||||
readonly isVersioned: boolean;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
interface InternalGetRoutesOptions {
|
||||
/** @default false */
|
||||
excludeVersionedRoutes?: boolean;
|
||||
}
|
||||
type RouterEvents =
|
||||
/** Called after route validation, regardless of success or failure */
|
||||
'onPostValidate';
|
||||
|
||||
/**
|
||||
* @internal
|
||||
|
@ -182,7 +184,8 @@ interface InternalGetRoutesOptions {
|
|||
export class Router<Context extends RequestHandlerContextBase = RequestHandlerContextBase>
|
||||
implements IRouter<Context>
|
||||
{
|
||||
public routes: Array<Readonly<InternalRouterRoute>> = [];
|
||||
private static ee = new EventEmitter();
|
||||
public routes: Array<Readonly<RouterRoute>> = [];
|
||||
public pluginId?: symbol;
|
||||
public get: InternalRegistrar<'get', Context>;
|
||||
public post: InternalRegistrar<'post', Context>;
|
||||
|
@ -202,25 +205,27 @@ export class Router<Context extends RequestHandlerContextBase = RequestHandlerCo
|
|||
<P, Q, B>(
|
||||
route: InternalRouteConfig<P, Q, B, Method>,
|
||||
handler: RequestHandler<P, Q, B, Context, Method>,
|
||||
{ isVersioned }: InternalRegistrarOptions = { isVersioned: false }
|
||||
{ isVersioned, events }: InternalRegistrarOptions = { isVersioned: false, events: true }
|
||||
) => {
|
||||
route = prepareRouteConfigValidation(route);
|
||||
const routeSchemas = routeSchemasFromRouteConfig(route, method);
|
||||
const isPublicUnversionedApi =
|
||||
const isPublicUnversionedRoute =
|
||||
!isVersioned &&
|
||||
route.options?.access === 'public' &&
|
||||
// We do not consider HTTP resource routes as APIs
|
||||
route.options?.httpResource !== true;
|
||||
|
||||
this.routes.push({
|
||||
handler: async (req, responseToolkit) =>
|
||||
await this.handle({
|
||||
handler: async (req, responseToolkit) => {
|
||||
return await this.handle({
|
||||
routeSchemas,
|
||||
request: req,
|
||||
responseToolkit,
|
||||
isPublicUnversionedApi,
|
||||
isPublicUnversionedRoute,
|
||||
handler: this.enhanceWithContext(handler),
|
||||
}),
|
||||
emit: events ? { onPostValidation: this.emitPostValidate } : undefined,
|
||||
});
|
||||
},
|
||||
method,
|
||||
path: getRouteFullPath(this.routerPath, route.path),
|
||||
options: validOptions(method, route),
|
||||
|
@ -229,6 +234,8 @@ export class Router<Context extends RequestHandlerContextBase = RequestHandlerCo
|
|||
? route.security
|
||||
: validRouteSecurity(route.security as DeepPartial<RouteSecurity>, route.options),
|
||||
validationSchemas: route.validate,
|
||||
// @ts-ignore using isVersioned: false in the type instead of boolean
|
||||
// for typeguarding between versioned and unversioned RouterRoute types
|
||||
isVersioned,
|
||||
});
|
||||
};
|
||||
|
@ -240,7 +247,15 @@ export class Router<Context extends RequestHandlerContextBase = RequestHandlerCo
|
|||
this.patch = buildMethod('patch');
|
||||
}
|
||||
|
||||
public getRoutes({ excludeVersionedRoutes }: InternalGetRoutesOptions = {}) {
|
||||
public static on(event: RouterEvents, cb: (req: CoreKibanaRequest, ...args: any[]) => void) {
|
||||
Router.ee.on(event, cb);
|
||||
}
|
||||
|
||||
public static off(event: RouterEvents, cb: (req: CoreKibanaRequest, ...args: any[]) => void) {
|
||||
Router.ee.off(event, cb);
|
||||
}
|
||||
|
||||
public getRoutes({ excludeVersionedRoutes }: { excludeVersionedRoutes?: boolean } = {}) {
|
||||
if (excludeVersionedRoutes) {
|
||||
return this.routes.filter((route) => !route.isVersioned);
|
||||
}
|
||||
|
@ -269,16 +284,29 @@ export class Router<Context extends RequestHandlerContextBase = RequestHandlerCo
|
|||
});
|
||||
}
|
||||
|
||||
/** Should be private, just exposed for convenience for the versioned router */
|
||||
public emitPostValidate = (
|
||||
request: KibanaRequest,
|
||||
routeOptions: { deprecated?: RouteDeprecationInfo } = {}
|
||||
) => {
|
||||
const postValidate: RouterEvents = 'onPostValidate';
|
||||
Router.ee.emit(postValidate, request, routeOptions);
|
||||
};
|
||||
|
||||
private async handle<P, Q, B>({
|
||||
routeSchemas,
|
||||
request,
|
||||
responseToolkit,
|
||||
isPublicUnversionedApi,
|
||||
emit,
|
||||
isPublicUnversionedRoute,
|
||||
handler,
|
||||
}: {
|
||||
request: Request;
|
||||
responseToolkit: ResponseToolkit;
|
||||
isPublicUnversionedApi: boolean;
|
||||
emit?: {
|
||||
onPostValidation: (req: KibanaRequest, reqOptions: any) => void;
|
||||
};
|
||||
isPublicUnversionedRoute: boolean;
|
||||
handler: RequestHandlerEnhanced<
|
||||
P,
|
||||
Q,
|
||||
|
@ -300,18 +328,24 @@ export class Router<Context extends RequestHandlerContextBase = RequestHandlerCo
|
|||
} catch (error) {
|
||||
this.logError('400 Bad Request', 400, { request, error });
|
||||
const response = hapiResponseAdapter.toBadRequest(error.message);
|
||||
if (isPublicUnversionedApi) {
|
||||
if (isPublicUnversionedRoute) {
|
||||
response.output.headers = {
|
||||
...response.output.headers,
|
||||
...getVersionHeader(ALLOWED_PUBLIC_VERSION),
|
||||
};
|
||||
}
|
||||
|
||||
// Emit onPostValidation even if validation fails.
|
||||
const req = CoreKibanaRequest.from(request);
|
||||
emit?.onPostValidation(req, req.route.options);
|
||||
return response;
|
||||
}
|
||||
|
||||
emit?.onPostValidation(kibanaRequest, kibanaRequest.route.options);
|
||||
|
||||
try {
|
||||
const kibanaResponse = await handler(kibanaRequest, kibanaResponseFactory);
|
||||
if (isPublicUnversionedApi) {
|
||||
if (isPublicUnversionedRoute) {
|
||||
injectVersionHeader(ALLOWED_PUBLIC_VERSION, kibanaResponse);
|
||||
}
|
||||
if (kibanaRequest.protocol === 'http2' && kibanaResponse.options.headers) {
|
||||
|
|
|
@ -198,7 +198,7 @@ describe('Versioned route', () => {
|
|||
expect(router.post).toHaveBeenCalledWith(
|
||||
expect.objectContaining(expectedRouteConfig),
|
||||
expect.any(Function),
|
||||
{ isVersioned: true }
|
||||
{ isVersioned: true, events: false }
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
@ -18,7 +18,6 @@ import type {
|
|||
KibanaRequest,
|
||||
KibanaResponseFactory,
|
||||
ApiVersion,
|
||||
AddVersionOpts,
|
||||
VersionedRoute,
|
||||
VersionedRouteConfig,
|
||||
IKibanaResponse,
|
||||
|
@ -26,9 +25,10 @@ import type {
|
|||
RouteSecurityGetter,
|
||||
RouteSecurity,
|
||||
RouteMethod,
|
||||
VersionedRouterRoute,
|
||||
} from '@kbn/core-http-server';
|
||||
import type { Mutable } from 'utility-types';
|
||||
import type { HandlerResolutionStrategy, Method, VersionedRouterRoute } from './types';
|
||||
import type { HandlerResolutionStrategy, Method, Options } from './types';
|
||||
|
||||
import { validate } from './validate';
|
||||
import {
|
||||
|
@ -46,8 +46,6 @@ import { prepareVersionedRouteValidation, unwrapVersionedResponseBodyValidation
|
|||
import type { RequestLike } from './route_version_utils';
|
||||
import { Router } from '../router';
|
||||
|
||||
type Options = AddVersionOpts<unknown, unknown, unknown>;
|
||||
|
||||
interface InternalVersionedRouteConfig<M extends RouteMethod> extends VersionedRouteConfig<M> {
|
||||
isDev: boolean;
|
||||
useVersionResolutionStrategyForInternalPaths: Map<string, boolean>;
|
||||
|
@ -68,7 +66,7 @@ function extractValidationSchemaFromHandler(handler: VersionedRouterRoute['handl
|
|||
}
|
||||
|
||||
export class CoreVersionedRoute implements VersionedRoute {
|
||||
private readonly handlers = new Map<
|
||||
public readonly handlers = new Map<
|
||||
ApiVersion,
|
||||
{
|
||||
fn: RequestHandler;
|
||||
|
@ -127,7 +125,7 @@ export class CoreVersionedRoute implements VersionedRoute {
|
|||
security: this.getSecurity,
|
||||
},
|
||||
this.requestHandler,
|
||||
{ isVersioned: true }
|
||||
{ isVersioned: true, events: false }
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -181,6 +179,7 @@ export class CoreVersionedRoute implements VersionedRoute {
|
|||
}
|
||||
const req = originalReq as Mutable<KibanaRequest>;
|
||||
const version = this.getVersion(req);
|
||||
req.apiVersion = version;
|
||||
|
||||
if (!version) {
|
||||
return res.badRequest({
|
||||
|
@ -221,6 +220,8 @@ export class CoreVersionedRoute implements VersionedRoute {
|
|||
req.params = params;
|
||||
req.query = query;
|
||||
} catch (e) {
|
||||
// Emit onPostValidation even if validation fails.
|
||||
this.router.emitPostValidate(req, handler.options.options);
|
||||
return res.badRequest({ body: e.message, headers: getVersionHeader(version) });
|
||||
}
|
||||
} else {
|
||||
|
@ -230,6 +231,8 @@ export class CoreVersionedRoute implements VersionedRoute {
|
|||
req.query = {};
|
||||
}
|
||||
|
||||
this.router.emitPostValidate(req, handler.options.options);
|
||||
|
||||
const response = await handler.fn(ctx, req, res);
|
||||
|
||||
if (this.isDev && validation?.response?.[response.status]?.body) {
|
||||
|
@ -280,7 +283,6 @@ export class CoreVersionedRoute implements VersionedRoute {
|
|||
public addVersion(options: Options, handler: RequestHandler<any, any, any, any>): VersionedRoute {
|
||||
this.validateVersion(options.version);
|
||||
options = prepareVersionedRouteValidation(options);
|
||||
|
||||
this.handlers.set(options.version, {
|
||||
fn: handler,
|
||||
options,
|
||||
|
|
|
@ -36,7 +36,6 @@ describe('Versioned router', () => {
|
|||
versionedRouter.get({
|
||||
path: '/test/{id}',
|
||||
access: 'internal',
|
||||
deprecated: true,
|
||||
discontinued: 'x.y.z',
|
||||
});
|
||||
versionedRouter.post({
|
||||
|
@ -50,16 +49,17 @@ describe('Versioned router', () => {
|
|||
Array [
|
||||
Object {
|
||||
"handlers": Array [],
|
||||
"isVersioned": true,
|
||||
"method": "get",
|
||||
"options": Object {
|
||||
"access": "internal",
|
||||
"deprecated": true,
|
||||
"discontinued": "x.y.z",
|
||||
},
|
||||
"path": "/test/{id}",
|
||||
},
|
||||
Object {
|
||||
"handlers": Array [],
|
||||
"isVersioned": true,
|
||||
"method": "post",
|
||||
"options": Object {
|
||||
"access": "internal",
|
||||
|
@ -70,6 +70,7 @@ describe('Versioned router', () => {
|
|||
},
|
||||
Object {
|
||||
"handlers": Array [],
|
||||
"isVersioned": true,
|
||||
"method": "delete",
|
||||
"options": Object {
|
||||
"access": "internal",
|
||||
|
|
|
@ -7,11 +7,16 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import type { VersionedRouter, VersionedRoute, VersionedRouteConfig } from '@kbn/core-http-server';
|
||||
import type {
|
||||
VersionedRouter,
|
||||
VersionedRoute,
|
||||
VersionedRouteConfig,
|
||||
VersionedRouterRoute,
|
||||
} from '@kbn/core-http-server';
|
||||
import { omit } from 'lodash';
|
||||
import { CoreVersionedRoute } from './core_versioned_route';
|
||||
import type { HandlerResolutionStrategy, Method, VersionedRouterRoute } from './types';
|
||||
import type { Router } from '../router';
|
||||
import type { HandlerResolutionStrategy, Method } from './types';
|
||||
import { getRouteFullPath, type Router } from '../router';
|
||||
|
||||
/** @internal */
|
||||
export interface VersionedRouterArgs {
|
||||
|
@ -98,10 +103,11 @@ export class CoreVersionedRouter implements VersionedRouter {
|
|||
public getRoutes(): VersionedRouterRoute[] {
|
||||
return [...this.routes].map((route) => {
|
||||
return {
|
||||
path: route.path,
|
||||
path: getRouteFullPath(this.router.routerPath, route.path),
|
||||
method: route.method,
|
||||
options: omit(route.options, 'path'),
|
||||
handlers: route.getHandlers(),
|
||||
isVersioned: true,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
|
|
@ -9,6 +9,6 @@
|
|||
|
||||
export { resolvers as versionHandlerResolvers } from './handler_resolvers';
|
||||
export { CoreVersionedRouter } from './core_versioned_router';
|
||||
export type { HandlerResolutionStrategy, VersionedRouterRoute } from './types';
|
||||
export type { HandlerResolutionStrategy } from './types';
|
||||
export { ALLOWED_PUBLIC_VERSION } from './route_version_utils';
|
||||
export { unwrapVersionedResponseBodyValidation } from './util';
|
||||
|
|
|
@ -20,6 +20,7 @@ export function createRouter(opts: CreateMockRouterOptions = {}) {
|
|||
put: jest.fn(),
|
||||
getRoutes: jest.fn(),
|
||||
handleLegacyErrors: jest.fn(),
|
||||
emitPostValidate: jest.fn(),
|
||||
patch: jest.fn(),
|
||||
routerPath: '',
|
||||
versioned: {} as any,
|
||||
|
|
|
@ -7,25 +7,12 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import type {
|
||||
AddVersionOpts,
|
||||
RequestHandler,
|
||||
RouteMethod,
|
||||
VersionedRouteConfig,
|
||||
} from '@kbn/core-http-server';
|
||||
import type { AddVersionOpts, RouteMethod } from '@kbn/core-http-server';
|
||||
|
||||
export type Method = Exclude<RouteMethod, 'options'>;
|
||||
|
||||
/** @internal */
|
||||
export interface VersionedRouterRoute {
|
||||
method: string;
|
||||
path: string;
|
||||
options: Omit<VersionedRouteConfig<RouteMethod>, 'path'>;
|
||||
handlers: Array<{
|
||||
fn: RequestHandler;
|
||||
options: AddVersionOpts<unknown, unknown, unknown>;
|
||||
}>;
|
||||
}
|
||||
export type Options = AddVersionOpts<unknown, unknown, unknown>;
|
||||
|
||||
/**
|
||||
* Specifies resolution strategy to use if a request does not provide a version.
|
||||
|
|
|
@ -14,6 +14,7 @@ import type {
|
|||
AddVersionOpts,
|
||||
RequestHandler,
|
||||
KibanaResponseFactory,
|
||||
VersionedRouterRoute,
|
||||
} from '@kbn/core-http-server';
|
||||
|
||||
export type MockedVersionedRoute = jest.Mocked<VersionedRoute>;
|
||||
|
@ -24,14 +25,16 @@ const createMockVersionedRoute = (): MockedVersionedRoute => {
|
|||
return api;
|
||||
};
|
||||
|
||||
type VersionedRouterMethods = keyof Omit<VersionedRouter, 'getRoutes'>;
|
||||
|
||||
export type MockedVersionedRouter = jest.Mocked<VersionedRouter<any>> & {
|
||||
getRoute: (method: keyof VersionedRouter, path: string) => RegisteredVersionedRoute;
|
||||
getRoute: (method: VersionedRouterMethods, path: string) => RegisteredVersionedRoute;
|
||||
};
|
||||
|
||||
const createMethodHandler = () => jest.fn((_) => createMockVersionedRoute());
|
||||
|
||||
const createMockGetRoutes = () => jest.fn(() => [] as VersionedRouterRoute[]);
|
||||
export const createVersionedRouterMock = (): MockedVersionedRouter => {
|
||||
const router: Omit<MockedVersionedRouter, 'getRoute'> = {
|
||||
const router: Omit<MockedVersionedRouter, 'getRoute' | 'getRoutes'> = {
|
||||
delete: createMethodHandler(),
|
||||
get: createMethodHandler(),
|
||||
patch: createMethodHandler(),
|
||||
|
@ -42,6 +45,7 @@ export const createVersionedRouterMock = (): MockedVersionedRouter => {
|
|||
return {
|
||||
...router,
|
||||
getRoute: getRoute.bind(null, router),
|
||||
getRoutes: createMockGetRoutes(),
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -54,9 +58,10 @@ export interface RegisteredVersionedRoute {
|
|||
};
|
||||
};
|
||||
}
|
||||
|
||||
const getRoute = (
|
||||
router: Omit<MockedVersionedRouter, 'getRoute'>,
|
||||
method: keyof VersionedRouter,
|
||||
router: Omit<MockedVersionedRouter, 'getRoute' | 'getRoutes'>,
|
||||
method: VersionedRouterMethods,
|
||||
path: string
|
||||
): RegisteredVersionedRoute => {
|
||||
if (!router[method].mock.calls.length) {
|
||||
|
|
|
@ -906,6 +906,7 @@ test('exposes route details of incoming request to a route handler', async () =>
|
|||
.expect(200, {
|
||||
method: 'get',
|
||||
path: '/',
|
||||
routePath: '/',
|
||||
options: {
|
||||
authRequired: true,
|
||||
xsrfRequired: false,
|
||||
|
@ -1088,6 +1089,7 @@ test('exposes route details of incoming request to a route handler (POST + paylo
|
|||
.expect(200, {
|
||||
method: 'post',
|
||||
path: '/',
|
||||
routePath: '/',
|
||||
options: {
|
||||
authRequired: true,
|
||||
xsrfRequired: true,
|
||||
|
|
|
@ -35,10 +35,12 @@ import type {
|
|||
HttpServerInfo,
|
||||
HttpAuth,
|
||||
IAuthHeadersStorage,
|
||||
RouterDeprecatedRouteDetails,
|
||||
RouteMethod,
|
||||
} from '@kbn/core-http-server';
|
||||
import { performance } from 'perf_hooks';
|
||||
import { isBoom } from '@hapi/boom';
|
||||
import { identity } from 'lodash';
|
||||
import { identity, isObject } from 'lodash';
|
||||
import { IHttpEluMonitorConfig } from '@kbn/core-http-server/src/elu_monitor';
|
||||
import { Env } from '@kbn/config';
|
||||
import { CoreContext } from '@kbn/core-base-server-internal';
|
||||
|
@ -140,6 +142,7 @@ export interface HttpServerSetup {
|
|||
registerAuth: HttpServiceSetup['registerAuth'];
|
||||
registerOnPostAuth: HttpServiceSetup['registerOnPostAuth'];
|
||||
registerOnPreResponse: HttpServiceSetup['registerOnPreResponse'];
|
||||
getDeprecatedRoutes: HttpServiceSetup['getDeprecatedRoutes'];
|
||||
authRequestHeaders: IAuthHeadersStorage;
|
||||
auth: HttpAuth;
|
||||
getServerInfo: () => HttpServerInfo;
|
||||
|
@ -280,6 +283,7 @@ export class HttpServer {
|
|||
|
||||
return {
|
||||
registerRouter: this.registerRouter.bind(this),
|
||||
getDeprecatedRoutes: this.getDeprecatedRoutes.bind(this),
|
||||
registerRouterAfterListening: this.registerRouterAfterListening.bind(this),
|
||||
registerStaticDir: this.registerStaticDir.bind(this),
|
||||
staticAssets,
|
||||
|
@ -385,6 +389,45 @@ export class HttpServer {
|
|||
}
|
||||
}
|
||||
|
||||
private getDeprecatedRoutes(): RouterDeprecatedRouteDetails[] {
|
||||
const deprecatedRoutes: RouterDeprecatedRouteDetails[] = [];
|
||||
|
||||
for (const router of this.registeredRouters) {
|
||||
const allRouterRoutes = [
|
||||
// exclude so we dont get double entries.
|
||||
// we need to call the versioned getRoutes to grab the full version options details
|
||||
router.getRoutes({ excludeVersionedRoutes: true }),
|
||||
router.versioned.getRoutes(),
|
||||
].flat();
|
||||
|
||||
deprecatedRoutes.push(
|
||||
...allRouterRoutes
|
||||
.flat()
|
||||
.map((route) => {
|
||||
if (route.isVersioned === true) {
|
||||
return [...route.handlers.entries()].map(([_, { options }]) => {
|
||||
const deprecated = options.options?.deprecated;
|
||||
return { route, version: `${options.version}`, deprecated };
|
||||
});
|
||||
}
|
||||
return { route, version: undefined, deprecated: route.options.deprecated };
|
||||
})
|
||||
.flat()
|
||||
.filter(({ deprecated }) => isObject(deprecated))
|
||||
.flatMap(({ route, deprecated, version }) => {
|
||||
return {
|
||||
routeDeprecationOptions: deprecated!,
|
||||
routeMethod: route.method as RouteMethod,
|
||||
routePath: route.path,
|
||||
routeVersion: version,
|
||||
};
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return deprecatedRoutes;
|
||||
}
|
||||
|
||||
private setupGracefulShutdownHandlers() {
|
||||
this.registerOnPreRouting((request, response, toolkit) => {
|
||||
if (this.stopping || this.stopped) {
|
||||
|
@ -693,12 +736,13 @@ export class HttpServer {
|
|||
this.log.debug(`registering route handler for [${route.path}]`);
|
||||
// Hapi does not allow payload validation to be specified for 'head' or 'get' requests
|
||||
const validate = isSafeMethod(route.method) ? undefined : { payload: true };
|
||||
const { authRequired, tags, body = {}, timeout } = route.options;
|
||||
const { authRequired, tags, body = {}, timeout, deprecated } = route.options;
|
||||
const { accepts: allow, override, maxBytes, output, parse } = body;
|
||||
|
||||
const kibanaRouteOptions: KibanaRouteOptions = {
|
||||
xsrfRequired: route.options.xsrfRequired ?? !isSafeMethod(route.method),
|
||||
access: route.options.access ?? 'internal',
|
||||
deprecated,
|
||||
security: route.security,
|
||||
};
|
||||
// Log HTTP API target consumer.
|
||||
|
|
|
@ -162,7 +162,7 @@ export class HttpService
|
|||
return this.internalPreboot;
|
||||
}
|
||||
|
||||
public async setup(deps: SetupDeps) {
|
||||
public async setup(deps: SetupDeps): Promise<InternalHttpServiceSetup> {
|
||||
this.requestHandlerContext = deps.context.createContextContainer();
|
||||
this.configSubscription = this.config$.subscribe(() => {
|
||||
if (this.httpServer.isListening()) {
|
||||
|
@ -185,9 +185,11 @@ export class HttpService
|
|||
|
||||
this.internalSetup = {
|
||||
...serverContract,
|
||||
|
||||
registerOnPostValidation: (cb) => {
|
||||
Router.on('onPostValidate', cb);
|
||||
},
|
||||
getRegisteredDeprecatedApis: () => serverContract.getDeprecatedRoutes(),
|
||||
externalUrl: new ExternalUrlConfig(config.externalUrl),
|
||||
|
||||
createRouter: <Context extends RequestHandlerContextBase = RequestHandlerContextBase>(
|
||||
path: string,
|
||||
pluginId: PluginOpaqueId = this.coreContext.coreId
|
||||
|
|
|
@ -16,7 +16,10 @@ import type {
|
|||
IContextContainer,
|
||||
HttpServiceSetup,
|
||||
HttpServiceStart,
|
||||
RouterDeprecatedRouteDetails,
|
||||
} from '@kbn/core-http-server';
|
||||
import { CoreKibanaRequest } from '@kbn/core-http-router-server-internal';
|
||||
import { RouteDeprecationInfo } from '@kbn/core-http-server/src/router/route';
|
||||
import type { HttpServerSetup } from './http_server';
|
||||
import type { ExternalUrlConfig } from './external_url';
|
||||
import type { InternalStaticAssets } from './static_assets';
|
||||
|
@ -54,6 +57,9 @@ export interface InternalHttpServiceSetup
|
|||
path: string,
|
||||
plugin?: PluginOpaqueId
|
||||
) => IRouter<Context>;
|
||||
registerOnPostValidation(
|
||||
cb: (req: CoreKibanaRequest, metadata: { deprecated: RouteDeprecationInfo }) => void
|
||||
): void;
|
||||
registerRouterAfterListening: (router: IRouter) => void;
|
||||
registerStaticDir: (path: string, dirPath: string) => void;
|
||||
authRequestHeaders: IAuthHeadersStorage;
|
||||
|
@ -65,6 +71,7 @@ export interface InternalHttpServiceSetup
|
|||
contextName: ContextName,
|
||||
provider: IContextProvider<Context, ContextName>
|
||||
) => IContextContainer;
|
||||
getRegisteredDeprecatedApis: () => RouterDeprecatedRouteDetails[];
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
|
|
|
@ -171,6 +171,9 @@ const createInternalSetupContractMock = () => {
|
|||
createCookieSessionStorageFactory: jest.fn(),
|
||||
registerOnPreRouting: jest.fn(),
|
||||
registerOnPreAuth: jest.fn(),
|
||||
getDeprecatedRoutes: jest.fn(),
|
||||
getRegisteredDeprecatedApis: jest.fn(),
|
||||
registerOnPostValidation: jest.fn(),
|
||||
registerAuth: jest.fn(),
|
||||
registerOnPostAuth: jest.fn(),
|
||||
registerRouteHandlerContext: jest.fn(),
|
||||
|
@ -207,6 +210,7 @@ const createSetupContractMock = <
|
|||
createCookieSessionStorageFactory: internalMock.createCookieSessionStorageFactory,
|
||||
registerOnPreRouting: internalMock.registerOnPreRouting,
|
||||
registerOnPreAuth: jest.fn(),
|
||||
getDeprecatedRoutes: jest.fn(),
|
||||
registerAuth: internalMock.registerAuth,
|
||||
registerOnPostAuth: internalMock.registerOnPostAuth,
|
||||
registerOnPreResponse: internalMock.registerOnPreResponse,
|
||||
|
|
|
@ -93,6 +93,7 @@ export type {
|
|||
IRouter,
|
||||
RouteRegistrar,
|
||||
RouterRoute,
|
||||
RouterDeprecatedRouteDetails,
|
||||
IKibanaSocket,
|
||||
KibanaErrorResponseFactory,
|
||||
KibanaRedirectionResponseFactory,
|
||||
|
@ -170,6 +171,7 @@ export type {
|
|||
VersionedRouter,
|
||||
VersionedRouteCustomResponseBodyValidation,
|
||||
VersionedResponseBodyValidation,
|
||||
VersionedRouterRoute,
|
||||
} from './src/versioning';
|
||||
|
||||
export type { IStaticAssets } from './src/static_assets';
|
||||
|
|
|
@ -12,6 +12,7 @@ import type {
|
|||
IContextProvider,
|
||||
IRouter,
|
||||
RequestHandlerContextBase,
|
||||
RouterDeprecatedRouteDetails,
|
||||
} from './router';
|
||||
import type {
|
||||
AuthenticationHandler,
|
||||
|
@ -359,6 +360,14 @@ export interface HttpServiceSetup<
|
|||
* Provides common {@link HttpServerInfo | information} about the running http server.
|
||||
*/
|
||||
getServerInfo: () => HttpServerInfo;
|
||||
|
||||
/**
|
||||
* Provides a list of all registered deprecated routes {{@link RouterDeprecatedRouteDetails | information}}.
|
||||
* The routers will be evaluated everytime this function gets called to
|
||||
* accommodate for any late route registrations
|
||||
* @returns {RouterDeprecatedRouteDetails[]}
|
||||
*/
|
||||
getDeprecatedRoutes: () => RouterDeprecatedRouteDetails[];
|
||||
}
|
||||
|
||||
/** @public */
|
||||
|
|
|
@ -80,7 +80,7 @@ export type {
|
|||
LazyValidator,
|
||||
} from './route_validator';
|
||||
export { RouteValidationError } from './route_validator';
|
||||
export type { IRouter, RouteRegistrar, RouterRoute } from './router';
|
||||
export type { IRouter, RouteRegistrar, RouterRoute, RouterDeprecatedRouteDetails } from './router';
|
||||
export type { IKibanaSocket } from './socket';
|
||||
export type {
|
||||
KibanaErrorResponseFactory,
|
||||
|
|
|
@ -13,7 +13,7 @@ import type { Observable } from 'rxjs';
|
|||
import type { RecursiveReadonly } from '@kbn/utility-types';
|
||||
import type { HttpProtocol } from '../http_contract';
|
||||
import type { IKibanaSocket } from './socket';
|
||||
import type { RouteMethod, RouteConfigOptions, RouteSecurity } from './route';
|
||||
import type { RouteMethod, RouteConfigOptions, RouteSecurity, RouteDeprecationInfo } from './route';
|
||||
import type { Headers } from './headers';
|
||||
|
||||
export type RouteSecurityGetter = (request: {
|
||||
|
@ -26,6 +26,7 @@ export type InternalRouteSecurity = RouteSecurity | RouteSecurityGetter;
|
|||
* @public
|
||||
*/
|
||||
export interface KibanaRouteOptions extends RouteOptionsApp {
|
||||
deprecated?: RouteDeprecationInfo;
|
||||
xsrfRequired: boolean;
|
||||
access: 'internal' | 'public';
|
||||
security?: InternalRouteSecurity;
|
||||
|
@ -59,6 +60,7 @@ export interface KibanaRequestRoute<Method extends RouteMethod> {
|
|||
path: string;
|
||||
method: Method;
|
||||
options: KibanaRequestRouteOptions<Method>;
|
||||
routePath?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -190,6 +192,11 @@ export interface KibanaRequest<
|
|||
*/
|
||||
readonly rewrittenUrl?: URL;
|
||||
|
||||
/**
|
||||
* The versioned route API version of this request.
|
||||
*/
|
||||
readonly apiVersion: string | undefined;
|
||||
|
||||
/**
|
||||
* The path parameter of this request.
|
||||
*/
|
||||
|
|
|
@ -113,6 +113,43 @@ export type RouteAccess = 'public' | 'internal';
|
|||
|
||||
export type Privilege = string;
|
||||
|
||||
/**
|
||||
* Route Deprecation info
|
||||
* This information will assist Kibana HTTP API users when upgrading to new versions
|
||||
* of the Elastic stack (via Upgrade Assistant) and will be surfaced in documentation
|
||||
* created from HTTP API introspection (like OAS).
|
||||
*/
|
||||
export interface RouteDeprecationInfo {
|
||||
documentationUrl: string;
|
||||
severity: 'warning' | 'critical';
|
||||
reason: VersionBumpDeprecationType | RemovalApiDeprecationType | MigrationApiDeprecationType;
|
||||
}
|
||||
|
||||
/**
|
||||
* bump deprecation reason denotes a new version of the API is available
|
||||
*/
|
||||
interface VersionBumpDeprecationType {
|
||||
type: 'bump';
|
||||
newApiVersion: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* remove deprecation reason denotes the API was fully removed with no replacement
|
||||
*/
|
||||
interface RemovalApiDeprecationType {
|
||||
type: 'remove';
|
||||
}
|
||||
|
||||
/**
|
||||
* migrate deprecation reason denotes the API has been migrated to a different API path
|
||||
* Please make sure that if you are only incrementing the version of the API to use 'bump' instead
|
||||
*/
|
||||
interface MigrationApiDeprecationType {
|
||||
type: 'migrate';
|
||||
newApiPath: string;
|
||||
newApiMethod: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* A set of privileges that can be used to define complex authorization requirements.
|
||||
*
|
||||
|
@ -277,12 +314,18 @@ export interface RouteConfigOptions<Method extends RouteMethod> {
|
|||
description?: string;
|
||||
|
||||
/**
|
||||
* Setting this to `true` declares this route to be deprecated. Consumers SHOULD
|
||||
* refrain from usage of this route.
|
||||
* Description of deprecations for this HTTP API.
|
||||
*
|
||||
* @remarks This will be surfaced in OAS documentation.
|
||||
* @remark This will assist Kibana HTTP API users when upgrading to new versions
|
||||
* of the Elastic stack (via Upgrade Assistant) and will be surfaced in documentation
|
||||
* created from HTTP API introspection (like OAS).
|
||||
*
|
||||
* Setting this object marks the route as deprecated.
|
||||
*
|
||||
* @remarks This may be surfaced in OAS documentation.
|
||||
* @public
|
||||
*/
|
||||
deprecated?: boolean;
|
||||
deprecated?: RouteDeprecationInfo;
|
||||
|
||||
/**
|
||||
* Whether this route should be treated as "invisible" and excluded from router
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
import type { Request, ResponseObject, ResponseToolkit } from '@hapi/hapi';
|
||||
import type Boom from '@hapi/boom';
|
||||
import type { VersionedRouter } from '../versioning';
|
||||
import type { RouteConfig, RouteMethod } from './route';
|
||||
import type { RouteConfig, RouteDeprecationInfo, RouteMethod } from './route';
|
||||
import type { RequestHandler, RequestHandlerWrapper } from './request_handler';
|
||||
import type { RequestHandlerContextBase } from './request_handler_context';
|
||||
import type { RouteConfigOptions } from './route';
|
||||
|
@ -98,7 +98,7 @@ export interface IRouter<Context extends RequestHandlerContextBase = RequestHand
|
|||
* @returns List of registered routes.
|
||||
* @internal
|
||||
*/
|
||||
getRoutes: () => RouterRoute[];
|
||||
getRoutes: (options?: { excludeVersionedRoutes?: boolean }) => RouterRoute[];
|
||||
|
||||
/**
|
||||
* An instance very similar to {@link IRouter} that can be used for versioning HTTP routes
|
||||
|
@ -139,4 +139,13 @@ export interface RouterRoute {
|
|||
req: Request,
|
||||
responseToolkit: ResponseToolkit
|
||||
) => Promise<ResponseObject | Boom.Boom<any>>;
|
||||
isVersioned: false;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export interface RouterDeprecatedRouteDetails {
|
||||
routeDeprecationOptions: RouteDeprecationInfo;
|
||||
routeMethod: RouteMethod;
|
||||
routePath: string;
|
||||
routeVersion?: string;
|
||||
}
|
||||
|
|
|
@ -19,4 +19,5 @@ export type {
|
|||
VersionedRouter,
|
||||
VersionedRouteCustomResponseBodyValidation,
|
||||
VersionedResponseBodyValidation,
|
||||
VersionedRouterRoute,
|
||||
} from './types';
|
||||
|
|
|
@ -20,7 +20,7 @@ import type {
|
|||
RouteValidationFunction,
|
||||
LazyValidator,
|
||||
} from '../..';
|
||||
|
||||
import type { RouteDeprecationInfo } from '../router/route';
|
||||
type RqCtx = RequestHandlerContextBase;
|
||||
|
||||
export type { ApiVersion };
|
||||
|
@ -89,17 +89,9 @@ export type VersionedRouteConfig<Method extends RouteMethod> = Omit<
|
|||
*/
|
||||
description?: string;
|
||||
|
||||
/**
|
||||
* Declares this operation to be deprecated. Consumers SHOULD refrain from usage
|
||||
* of this route. This will be surfaced in OAS documentation.
|
||||
*
|
||||
* @default false
|
||||
*/
|
||||
deprecated?: boolean;
|
||||
|
||||
/**
|
||||
* Release version or date that this route will be removed
|
||||
* Use with `deprecated: true`
|
||||
* Use with `deprecated: {@link RouteDeprecationInfo}`
|
||||
*
|
||||
* @default undefined
|
||||
*/
|
||||
|
@ -234,6 +226,11 @@ export interface VersionedRouter<Ctx extends RqCtx = RqCtx> {
|
|||
* @track-adoption
|
||||
*/
|
||||
delete: VersionedRouteRegistrar<'delete', Ctx>;
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
getRoutes: () => VersionedRouterRoute[];
|
||||
}
|
||||
|
||||
/** @public */
|
||||
|
@ -341,6 +338,10 @@ export interface AddVersionOpts<P, Q, B> {
|
|||
validate: false | VersionedRouteValidation<P, Q, B> | (() => VersionedRouteValidation<P, Q, B>); // Provide a way to lazily load validation schemas
|
||||
|
||||
security?: Exclude<RouteConfigOptions<RouteMethod>['security'], undefined>;
|
||||
|
||||
options?: {
|
||||
deprecated?: RouteDeprecationInfo;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -363,3 +364,11 @@ export interface VersionedRoute<
|
|||
handler: (...params: Parameters<RequestHandler<P, Q, B, Ctx>>) => MaybePromise<IKibanaResponse>
|
||||
): VersionedRoute<Method, Ctx>;
|
||||
}
|
||||
|
||||
export interface VersionedRouterRoute<P = unknown, Q = unknown, B = unknown> {
|
||||
method: string;
|
||||
path: string;
|
||||
options: Omit<VersionedRouteConfig<RouteMethod>, 'path'>;
|
||||
handlers: Array<{ fn: RequestHandler<P, Q, B>; options: AddVersionOpts<P, Q, B> }>;
|
||||
isVersioned: true;
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
"@kbn/utility-types",
|
||||
"@kbn/core-base-common",
|
||||
"@kbn/core-http-common",
|
||||
"@kbn/zod"
|
||||
"@kbn/zod",
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*",
|
||||
|
|
|
@ -76,6 +76,8 @@ export function createCoreSetupMock({
|
|||
userProfile: userProfileServiceMock.createSetup(),
|
||||
coreUsageData: {
|
||||
registerUsageCounter: coreUsageDataServiceMock.createSetupContract().registerUsageCounter,
|
||||
registerDeprecatedUsageFetch:
|
||||
coreUsageDataServiceMock.createSetupContract().registerDeprecatedUsageFetch,
|
||||
},
|
||||
plugins: {
|
||||
onSetup: jest.fn(),
|
||||
|
|
|
@ -225,6 +225,7 @@ export function createPluginSetupContext<TPlugin, TPluginDependencies>({
|
|||
},
|
||||
http: {
|
||||
createCookieSessionStorageFactory: deps.http.createCookieSessionStorageFactory,
|
||||
getDeprecatedRoutes: deps.http.getDeprecatedRoutes,
|
||||
registerRouteHandlerContext: <
|
||||
Context extends RequestHandlerContext,
|
||||
ContextName extends keyof Omit<Context, 'resolve'>
|
||||
|
@ -283,6 +284,7 @@ export function createPluginSetupContext<TPlugin, TPluginDependencies>({
|
|||
deprecations: deps.deprecations.getRegistry(plugin.name),
|
||||
coreUsageData: {
|
||||
registerUsageCounter: deps.coreUsageData.registerUsageCounter,
|
||||
registerDeprecatedUsageFetch: deps.coreUsageData.registerDeprecatedUsageFetch,
|
||||
},
|
||||
plugins: {
|
||||
onSetup: (...dependencyNames) => runtimeResolver.onSetup(plugin.name, dependencyNames),
|
||||
|
|
|
@ -276,10 +276,6 @@ export class Server {
|
|||
executionContext: executionContextSetup,
|
||||
});
|
||||
|
||||
const deprecationsSetup = await this.deprecations.setup({
|
||||
http: httpSetup,
|
||||
});
|
||||
|
||||
// setup i18n prior to any other service, to have translations ready
|
||||
const i18nServiceSetup = await this.i18n.setup({ http: httpSetup, pluginPaths });
|
||||
|
||||
|
@ -303,6 +299,11 @@ export class Server {
|
|||
changedDeprecatedConfigPath$: this.configService.getDeprecatedConfigPath$(),
|
||||
});
|
||||
|
||||
const deprecationsSetup = await this.deprecations.setup({
|
||||
http: httpSetup,
|
||||
coreUsageData: coreUsageDataSetup,
|
||||
});
|
||||
|
||||
const savedObjectsSetup = await this.savedObjects.setup({
|
||||
http: httpSetup,
|
||||
elasticsearch: elasticsearchServiceSetup,
|
||||
|
|
|
@ -36,6 +36,7 @@ export const createCoreUsageDataSetupMock = () => {
|
|||
getClient: jest.fn(),
|
||||
registerUsageCounter: jest.fn(),
|
||||
incrementUsageCounter: jest.fn(),
|
||||
registerDeprecatedUsageFetch: jest.fn(),
|
||||
};
|
||||
return setupContract;
|
||||
};
|
||||
|
|
|
@ -38,6 +38,7 @@ export const registerBulkCreateRoute = (
|
|||
summary: `Create saved objects`,
|
||||
tags: ['oas-tag:saved objects'],
|
||||
access,
|
||||
// @ts-expect-error TODO(https://github.com/elastic/kibana/issues/196095): Replace {RouteDeprecationInfo}
|
||||
deprecated: true,
|
||||
},
|
||||
validate: {
|
||||
|
|
|
@ -38,6 +38,7 @@ export const registerBulkDeleteRoute = (
|
|||
summary: `Delete saved objects`,
|
||||
tags: ['oas-tag:saved objects'],
|
||||
access,
|
||||
// @ts-expect-error TODO(https://github.com/elastic/kibana/issues/196095): Replace {RouteDeprecationInfo}
|
||||
deprecated: true,
|
||||
},
|
||||
validate: {
|
||||
|
|
|
@ -38,6 +38,7 @@ export const registerBulkGetRoute = (
|
|||
summary: `Get saved objects`,
|
||||
tags: ['oas-tag:saved objects'],
|
||||
access,
|
||||
// @ts-expect-error TODO(https://github.com/elastic/kibana/issues/196095): Replace {RouteDeprecationInfo}
|
||||
deprecated: true,
|
||||
},
|
||||
validate: {
|
||||
|
|
|
@ -38,6 +38,7 @@ export const registerBulkResolveRoute = (
|
|||
summary: `Resolve saved objects`,
|
||||
tags: ['oas-tag:saved objects'],
|
||||
access,
|
||||
// @ts-expect-error TODO(https://github.com/elastic/kibana/issues/196095): Replace {RouteDeprecationInfo}
|
||||
deprecated: true,
|
||||
description: `Retrieve multiple Kibana saved objects by ID, using any legacy URL aliases if they exist.
|
||||
Under certain circumstances, when Kibana is upgraded, saved object migrations may necessitate regenerating some object IDs to enable new features. When an object's ID is regenerated, a legacy URL alias is created for that object, preserving its old ID. In such a scenario, that object can be retrieved with the bulk resolve API using either its new ID or its old ID.`,
|
||||
|
|
|
@ -38,6 +38,7 @@ export const registerBulkUpdateRoute = (
|
|||
summary: `Update saved objects`,
|
||||
tags: ['oas-tag:saved objects'],
|
||||
access,
|
||||
// @ts-expect-error TODO(https://github.com/elastic/kibana/issues/196095): Replace {RouteDeprecationInfo}
|
||||
deprecated: true,
|
||||
},
|
||||
validate: {
|
||||
|
|
|
@ -38,6 +38,7 @@ export const registerCreateRoute = (
|
|||
summary: `Create a saved object`,
|
||||
tags: ['oas-tag:saved objects'],
|
||||
access,
|
||||
// @ts-expect-error TODO(https://github.com/elastic/kibana/issues/196095): Replace {RouteDeprecationInfo}
|
||||
deprecated: true,
|
||||
},
|
||||
validate: {
|
||||
|
|
|
@ -38,6 +38,7 @@ export const registerDeleteRoute = (
|
|||
summary: `Delete a saved object`,
|
||||
tags: ['oas-tag:saved objects'],
|
||||
access,
|
||||
// @ts-expect-error TODO(https://github.com/elastic/kibana/issues/196095): Replace {RouteDeprecationInfo}
|
||||
deprecated: true,
|
||||
},
|
||||
validate: {
|
||||
|
|
|
@ -42,6 +42,7 @@ export const registerFindRoute = (
|
|||
summary: `Search for saved objects`,
|
||||
tags: ['oas-tag:saved objects'],
|
||||
access,
|
||||
// @ts-expect-error TODO(https://github.com/elastic/kibana/issues/196095): Replace {RouteDeprecationInfo}
|
||||
deprecated: true,
|
||||
},
|
||||
validate: {
|
||||
|
|
|
@ -38,6 +38,7 @@ export const registerGetRoute = (
|
|||
summary: `Get a saved object`,
|
||||
tags: ['oas-tag:saved objects'],
|
||||
access,
|
||||
// @ts-expect-error TODO(https://github.com/elastic/kibana/issues/196095): Replace {RouteDeprecationInfo}
|
||||
deprecated: true,
|
||||
},
|
||||
validate: {
|
||||
|
|
|
@ -34,6 +34,7 @@ export const registerResolveRoute = (
|
|||
summary: `Resolve a saved object`,
|
||||
tags: ['oas-tag:saved objects'],
|
||||
access,
|
||||
// @ts-expect-error TODO(https://github.com/elastic/kibana/issues/196095): Replace {RouteDeprecationInfo}
|
||||
deprecated: true,
|
||||
description: `Retrieve a single Kibana saved object by ID, using any legacy URL alias if it exists.
|
||||
Under certain circumstances, when Kibana is upgraded, saved object migrations may necessitate regenerating some object IDs to enable new features. When an object's ID is regenerated, a legacy URL alias is created for that object, preserving its old ID. In such a scenario, that object can be retrieved with the resolve API using either its new ID or its old ID.`,
|
||||
|
|
|
@ -39,6 +39,7 @@ export const registerUpdateRoute = (
|
|||
summary: `Update a saved object`,
|
||||
tags: ['oas-tag:saved objects'],
|
||||
access,
|
||||
// @ts-expect-error TODO(https://github.com/elastic/kibana/issues/196095): Replace {RouteDeprecationInfo}
|
||||
deprecated: true,
|
||||
},
|
||||
validate: {
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
*/
|
||||
|
||||
import type { KibanaRequest } from '@kbn/core-http-server';
|
||||
import type { CoreUsageStats } from '@kbn/core-usage-data-server';
|
||||
import type { CoreUsageStats, CoreDeprecatedApiUsageStats } from '@kbn/core-usage-data-server';
|
||||
|
||||
/** @internal */
|
||||
export interface BaseIncrementOptions {
|
||||
|
@ -38,6 +38,13 @@ export type IncrementSavedObjectsExportOptions = BaseIncrementOptions & {
|
|||
export interface ICoreUsageStatsClient {
|
||||
getUsageStats(): Promise<CoreUsageStats>;
|
||||
|
||||
getDeprecatedApiUsageStats(): Promise<CoreDeprecatedApiUsageStats[]>;
|
||||
|
||||
incrementDeprecatedApi(
|
||||
counterName: string,
|
||||
options: { resolved?: boolean; incrementBy?: number }
|
||||
): Promise<void>;
|
||||
|
||||
incrementSavedObjectsBulkCreate(options: BaseIncrementOptions): Promise<void>;
|
||||
|
||||
incrementSavedObjectsBulkGet(options: BaseIncrementOptions): Promise<void>;
|
||||
|
|
|
@ -37,6 +37,7 @@ import type {
|
|||
CoreConfigUsageData,
|
||||
CoreIncrementCounterParams,
|
||||
CoreUsageCounter,
|
||||
DeprecatedApiUsageFetcher,
|
||||
} from '@kbn/core-usage-data-server';
|
||||
import {
|
||||
CORE_USAGE_STATS_TYPE,
|
||||
|
@ -48,6 +49,7 @@ import {
|
|||
type SavedObjectsServiceStart,
|
||||
} from '@kbn/core-saved-objects-server';
|
||||
|
||||
import { ISavedObjectsRepository } from '@kbn/core-saved-objects-api-server';
|
||||
import { isConfigured } from './is_configured';
|
||||
import { coreUsageStatsType } from './saved_objects';
|
||||
import { CoreUsageStatsClient } from './core_usage_stats_client';
|
||||
|
@ -88,6 +90,7 @@ export class CoreUsageDataService
|
|||
private coreUsageStatsClient?: CoreUsageStatsClient;
|
||||
private deprecatedConfigPaths: ChangedDeprecatedPaths = { set: [], unset: [] };
|
||||
private incrementUsageCounter: CoreIncrementUsageCounter = () => {}; // Initially set to noop
|
||||
private deprecatedApiUsageFetcher: DeprecatedApiUsageFetcher = async () => []; // Initially set to noop
|
||||
|
||||
constructor(core: CoreContext) {
|
||||
this.logger = core.logger.get('core-usage-stats-service');
|
||||
|
@ -513,12 +516,21 @@ export class CoreUsageDataService
|
|||
}
|
||||
};
|
||||
|
||||
const registerDeprecatedUsageFetch = (fetchFn: DeprecatedApiUsageFetcher) => {
|
||||
this.deprecatedApiUsageFetcher = fetchFn;
|
||||
};
|
||||
|
||||
const fetchDeprecatedUsageStats = (params: { soClient: ISavedObjectsRepository }) => {
|
||||
return this.deprecatedApiUsageFetcher(params);
|
||||
};
|
||||
|
||||
this.coreUsageStatsClient = new CoreUsageStatsClient({
|
||||
debugLogger: (message: string) => this.logger.debug(message),
|
||||
basePath: http.basePath,
|
||||
repositoryPromise: internalRepositoryPromise,
|
||||
stop$: this.stop$,
|
||||
incrementUsageCounter,
|
||||
fetchDeprecatedUsageStats,
|
||||
});
|
||||
|
||||
const contract: InternalCoreUsageDataSetup = {
|
||||
|
@ -526,6 +538,7 @@ export class CoreUsageDataService
|
|||
getClient: () => this.coreUsageStatsClient!,
|
||||
registerUsageCounter,
|
||||
incrementUsageCounter,
|
||||
registerDeprecatedUsageFetch,
|
||||
};
|
||||
|
||||
return contract;
|
||||
|
|
|
@ -52,6 +52,7 @@ describe('CoreUsageStatsClient', () => {
|
|||
debugLogger: debugLoggerMock,
|
||||
basePath: basePathMock,
|
||||
repositoryPromise: Promise.resolve(repositoryMock),
|
||||
fetchDeprecatedUsageStats: jest.fn(),
|
||||
stop$,
|
||||
incrementUsageCounter: incrementUsageCounterMock,
|
||||
});
|
||||
|
|
|
@ -37,6 +37,7 @@ import {
|
|||
takeUntil,
|
||||
tap,
|
||||
} from 'rxjs';
|
||||
import type { DeprecatedApiUsageFetcher } from '@kbn/core-usage-data-server';
|
||||
|
||||
export const BULK_CREATE_STATS_PREFIX = 'apiCalls.savedObjectsBulkCreate';
|
||||
export const BULK_GET_STATS_PREFIX = 'apiCalls.savedObjectsBulkGet';
|
||||
|
@ -108,6 +109,16 @@ export interface CoreUsageEvent {
|
|||
types?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface that models core events triggered by API deprecations. (e.g. SO HTTP API calls)
|
||||
* @internal
|
||||
*/
|
||||
export interface CoreUsageDeprecatedApiEvent {
|
||||
id: string;
|
||||
resolved: boolean;
|
||||
incrementBy: number;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export interface CoreUsageStatsClientParams {
|
||||
debugLogger: (message: string) => void;
|
||||
|
@ -116,6 +127,7 @@ export interface CoreUsageStatsClientParams {
|
|||
stop$: Observable<void>;
|
||||
incrementUsageCounter: (params: CoreIncrementCounterParams) => void;
|
||||
bufferTimeMs?: number;
|
||||
fetchDeprecatedUsageStats: DeprecatedApiUsageFetcher;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
|
@ -126,6 +138,8 @@ export class CoreUsageStatsClient implements ICoreUsageStatsClient {
|
|||
private readonly fieldsToIncrement$ = new Subject<string[]>();
|
||||
private readonly flush$ = new Subject<void>();
|
||||
private readonly coreUsageEvents$ = new Subject<CoreUsageEvent>();
|
||||
private readonly coreUsageDeprecatedApiCalls$ = new Subject<CoreUsageDeprecatedApiEvent>();
|
||||
private readonly fetchDeprecatedUsageStats: DeprecatedApiUsageFetcher;
|
||||
|
||||
constructor({
|
||||
debugLogger,
|
||||
|
@ -134,10 +148,12 @@ export class CoreUsageStatsClient implements ICoreUsageStatsClient {
|
|||
stop$,
|
||||
incrementUsageCounter,
|
||||
bufferTimeMs = DEFAULT_BUFFER_TIME_MS,
|
||||
fetchDeprecatedUsageStats,
|
||||
}: CoreUsageStatsClientParams) {
|
||||
this.debugLogger = debugLogger;
|
||||
this.basePath = basePath;
|
||||
this.repositoryPromise = repositoryPromise;
|
||||
this.fetchDeprecatedUsageStats = fetchDeprecatedUsageStats;
|
||||
this.fieldsToIncrement$
|
||||
.pipe(
|
||||
takeUntil(stop$),
|
||||
|
@ -180,6 +196,28 @@ export class CoreUsageStatsClient implements ICoreUsageStatsClient {
|
|||
)
|
||||
.subscribe();
|
||||
|
||||
this.coreUsageDeprecatedApiCalls$
|
||||
.pipe(
|
||||
takeUntil(stop$),
|
||||
tap(({ id, incrementBy, resolved }) => {
|
||||
incrementUsageCounter({
|
||||
counterName: id,
|
||||
counterType: `deprecated_api_call:${resolved ? 'resolved' : 'total'}`,
|
||||
incrementBy,
|
||||
});
|
||||
|
||||
if (resolved) {
|
||||
// increment number of times the marked_as_resolve has been called
|
||||
incrementUsageCounter({
|
||||
counterName: id,
|
||||
counterType: 'deprecated_api_call:marked_as_resolved',
|
||||
incrementBy: 1,
|
||||
});
|
||||
}
|
||||
})
|
||||
)
|
||||
.subscribe();
|
||||
|
||||
this.coreUsageEvents$
|
||||
.pipe(
|
||||
takeUntil(stop$),
|
||||
|
@ -215,6 +253,20 @@ export class CoreUsageStatsClient implements ICoreUsageStatsClient {
|
|||
return coreUsageStats;
|
||||
}
|
||||
|
||||
public async incrementDeprecatedApi(
|
||||
id: string,
|
||||
{ resolved = false, incrementBy = 1 }: { resolved: boolean; incrementBy: number }
|
||||
) {
|
||||
const deprecatedField = resolved ? 'deprecated_api_calls_resolved' : 'deprecated_api_calls';
|
||||
this.coreUsageDeprecatedApiCalls$.next({ id, resolved, incrementBy });
|
||||
this.fieldsToIncrement$.next([`${deprecatedField}.total`]);
|
||||
}
|
||||
|
||||
public async getDeprecatedApiUsageStats() {
|
||||
const repository = await this.repositoryPromise;
|
||||
return await this.fetchDeprecatedUsageStats({ soClient: repository });
|
||||
}
|
||||
|
||||
public async incrementSavedObjectsBulkCreate(options: BaseIncrementOptions) {
|
||||
await this.updateUsageStats([], BULK_CREATE_STATS_PREFIX, options);
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@ import { coreUsageStatsClientMock } from './core_usage_stats_client.mock';
|
|||
const createSetupContractMock = (usageStatsClient = coreUsageStatsClientMock.create()) => {
|
||||
const setupContract: jest.Mocked<InternalCoreUsageDataSetup> = {
|
||||
registerType: jest.fn(),
|
||||
registerDeprecatedUsageFetch: jest.fn(),
|
||||
getClient: jest.fn().mockReturnValue(usageStatsClient),
|
||||
registerUsageCounter: jest.fn(),
|
||||
incrementUsageCounter: jest.fn(),
|
||||
|
|
|
@ -12,6 +12,7 @@ import type { ICoreUsageStatsClient } from '@kbn/core-usage-data-base-server-int
|
|||
const createUsageStatsClientMock = () =>
|
||||
({
|
||||
getUsageStats: jest.fn().mockResolvedValue({}),
|
||||
getDeprecatedApiUsageStats: jest.fn().mockResolvedValue([]),
|
||||
incrementSavedObjectsBulkCreate: jest.fn().mockResolvedValue(null),
|
||||
incrementSavedObjectsBulkGet: jest.fn().mockResolvedValue(null),
|
||||
incrementSavedObjectsBulkResolve: jest.fn().mockResolvedValue(null),
|
||||
|
|
|
@ -19,4 +19,6 @@ export type {
|
|||
CoreConfigUsageData,
|
||||
CoreServicesUsageData,
|
||||
CoreUsageStats,
|
||||
CoreDeprecatedApiUsageStats,
|
||||
DeprecatedApiUsageFetcher,
|
||||
} from './src';
|
||||
|
|
|
@ -146,3 +146,16 @@ export interface CoreUsageStats {
|
|||
'savedObjectsRepository.resolvedOutcome.notFound'?: number;
|
||||
'savedObjectsRepository.resolvedOutcome.total'?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*
|
||||
* CoreDeprecatedApiUsageStats are collected over time while Kibana is running.
|
||||
*/
|
||||
export interface CoreDeprecatedApiUsageStats {
|
||||
apiId: string;
|
||||
totalMarkedAsResolved: number;
|
||||
markedAsResolvedLastCalledAt: string;
|
||||
apiTotalCalls: number;
|
||||
apiLastCalledAt: string;
|
||||
}
|
||||
|
|
|
@ -12,11 +12,12 @@ export type {
|
|||
CoreEnvironmentUsageData,
|
||||
CoreConfigUsageData,
|
||||
} from './core_usage_data';
|
||||
export type { CoreUsageStats } from './core_usage_stats';
|
||||
export type { CoreUsageStats, CoreDeprecatedApiUsageStats } from './core_usage_stats';
|
||||
export type {
|
||||
CoreUsageDataSetup,
|
||||
CoreUsageCounter,
|
||||
CoreIncrementUsageCounter,
|
||||
CoreIncrementCounterParams,
|
||||
DeprecatedApiUsageFetcher,
|
||||
} from './setup_contract';
|
||||
export type { CoreUsageData, ConfigUsageData, CoreUsageDataStart } from './start_contract';
|
||||
|
|
|
@ -7,12 +7,12 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import type { ISavedObjectsRepository } from '@kbn/core-saved-objects-api-server';
|
||||
import type { CoreDeprecatedApiUsageStats } from './core_usage_stats';
|
||||
|
||||
/**
|
||||
* Internal API for registering the Usage Tracker used for Core's usage data payload.
|
||||
*
|
||||
* @note This API should never be used to drive application logic and is only
|
||||
* intended for telemetry purposes.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface CoreUsageDataSetup {
|
||||
|
@ -21,6 +21,7 @@ export interface CoreUsageDataSetup {
|
|||
* when tracking events.
|
||||
*/
|
||||
registerUsageCounter: (usageCounter: CoreUsageCounter) => void;
|
||||
registerDeprecatedUsageFetch: (fetchFn: DeprecatedApiUsageFetcher) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -49,3 +50,11 @@ export interface CoreIncrementCounterParams {
|
|||
* Method to call whenever an event occurs, so the counter can be increased.
|
||||
*/
|
||||
export type CoreIncrementUsageCounter = (params: CoreIncrementCounterParams) => void;
|
||||
|
||||
/**
|
||||
* @public
|
||||
* Registers the deprecated API fetcher to be called to grab all the deprecated API usage details.
|
||||
*/
|
||||
export type DeprecatedApiUsageFetcher = (params: {
|
||||
soClient: ISavedObjectsRepository;
|
||||
}) => Promise<CoreDeprecatedApiUsageStats[]>;
|
||||
|
|
|
@ -12,5 +12,8 @@
|
|||
],
|
||||
"exclude": [
|
||||
"target/**/*",
|
||||
],
|
||||
"kbn_references": [
|
||||
"@kbn/core-saved-objects-api-server",
|
||||
]
|
||||
}
|
||||
|
|
|
@ -572,6 +572,7 @@ OK response oas-test-version-2",
|
|||
},
|
||||
"/no-xsrf/{id}/{path*}": Object {
|
||||
"post": Object {
|
||||
"deprecated": true,
|
||||
"operationId": "%2Fno-xsrf%2F%7Bid%7D%2F%7Bpath*%7D#1",
|
||||
"parameters": Array [
|
||||
Object {
|
||||
|
|
|
@ -189,6 +189,7 @@ describe('generateOpenApiDocument', () => {
|
|||
versionedRouters: { testVersionedRouter: { routes: [{}] } },
|
||||
bodySchema: createSharedZodSchema(),
|
||||
});
|
||||
|
||||
expect(
|
||||
generateOpenApiDocument(
|
||||
{
|
||||
|
@ -240,6 +241,7 @@ describe('generateOpenApiDocument', () => {
|
|||
{
|
||||
method: 'get',
|
||||
path: '/test',
|
||||
isVersioned: true,
|
||||
options: { access: 'public' },
|
||||
handlers: [
|
||||
{
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
import type { ZodType } from '@kbn/zod';
|
||||
import { schema, Type } from '@kbn/config-schema';
|
||||
import type { CoreVersionedRouter, Router } from '@kbn/core-http-router-server-internal';
|
||||
import type { RouterRoute, VersionedRouterRoute } from '@kbn/core-http-server';
|
||||
import { createLargeSchema } from './oas_converter/kbn_config_schema/lib.test.util';
|
||||
|
||||
type RoutesMeta = ReturnType<Router['getRoutes']>[number];
|
||||
|
@ -27,7 +28,7 @@ export const createVersionedRouter = (args: { routes: VersionedRoutesMeta[] }) =
|
|||
} as unknown as CoreVersionedRouter;
|
||||
};
|
||||
|
||||
export const getRouterDefaults = (bodySchema?: RuntimeSchema) => ({
|
||||
export const getRouterDefaults = (bodySchema?: RuntimeSchema): RouterRoute => ({
|
||||
isVersioned: false,
|
||||
path: '/foo/{id}/{path*}',
|
||||
method: 'get',
|
||||
|
@ -57,22 +58,29 @@ export const getRouterDefaults = (bodySchema?: RuntimeSchema) => ({
|
|||
handler: jest.fn(),
|
||||
});
|
||||
|
||||
export const getVersionedRouterDefaults = (bodySchema?: RuntimeSchema) => ({
|
||||
export const getVersionedRouterDefaults = (bodySchema?: RuntimeSchema): VersionedRouterRoute => ({
|
||||
method: 'get',
|
||||
path: '/bar',
|
||||
options: {
|
||||
summary: 'versioned route',
|
||||
access: 'public',
|
||||
deprecated: true,
|
||||
discontinued: 'route discontinued version or date',
|
||||
options: {
|
||||
tags: ['ignore-me', 'oas-tag:versioned'],
|
||||
},
|
||||
},
|
||||
isVersioned: true,
|
||||
handlers: [
|
||||
{
|
||||
fn: jest.fn(),
|
||||
options: {
|
||||
options: {
|
||||
deprecated: {
|
||||
documentationUrl: 'https://fake-url',
|
||||
reason: { type: 'remove' },
|
||||
severity: 'critical',
|
||||
},
|
||||
},
|
||||
validate: {
|
||||
request: {
|
||||
body:
|
||||
|
|
|
@ -63,11 +63,12 @@ export const processRouter = (
|
|||
parameters.push(...pathObjects, ...queryObjects);
|
||||
}
|
||||
|
||||
const hasDeprecations = !!route.options.deprecated;
|
||||
const operation: CustomOperationObject = {
|
||||
summary: route.options.summary ?? '',
|
||||
tags: route.options.tags ? extractTags(route.options.tags) : [],
|
||||
...(route.options.description ? { description: route.options.description } : {}),
|
||||
...(route.options.deprecated ? { deprecated: route.options.deprecated } : {}),
|
||||
...(hasDeprecations ? { deprecated: true } : {}),
|
||||
...(route.options.discontinued ? { 'x-discontinued': route.options.discontinued } : {}),
|
||||
requestBody: !!validationSchemas?.body
|
||||
? {
|
||||
|
|
|
@ -8,10 +8,7 @@
|
|||
*/
|
||||
|
||||
import { schema } from '@kbn/config-schema';
|
||||
import type {
|
||||
CoreVersionedRouter,
|
||||
VersionedRouterRoute,
|
||||
} from '@kbn/core-http-router-server-internal';
|
||||
import type { CoreVersionedRouter } from '@kbn/core-http-router-server-internal';
|
||||
import { get } from 'lodash';
|
||||
import { OasConverter } from './oas_converter';
|
||||
import { createOperationIdCounter } from './operation_id_counter';
|
||||
|
@ -20,6 +17,7 @@ import {
|
|||
extractVersionedResponses,
|
||||
extractVersionedRequestBodies,
|
||||
} from './process_versioned_router';
|
||||
import { VersionedRouterRoute } from '@kbn/core-http-server';
|
||||
|
||||
let oasConverter: OasConverter;
|
||||
beforeEach(() => {
|
||||
|
@ -151,6 +149,7 @@ describe('processVersionedRouter', () => {
|
|||
const createTestRoute: () => VersionedRouterRoute = () => ({
|
||||
path: '/foo',
|
||||
method: 'get',
|
||||
isVersioned: true,
|
||||
options: {
|
||||
access: 'public',
|
||||
deprecated: true,
|
||||
|
|
|
@ -10,10 +10,9 @@
|
|||
import {
|
||||
type CoreVersionedRouter,
|
||||
versionHandlerResolvers,
|
||||
VersionedRouterRoute,
|
||||
unwrapVersionedResponseBodyValidation,
|
||||
} from '@kbn/core-http-router-server-internal';
|
||||
import type { RouteMethod } from '@kbn/core-http-server';
|
||||
import type { RouteMethod, VersionedRouterRoute } from '@kbn/core-http-server';
|
||||
import type { OpenAPIV3 } from 'openapi-types';
|
||||
import type { GenerateOpenApiDocumentOptionsFilters } from './generate_oas';
|
||||
import type { OasConverter } from './oas_converter';
|
||||
|
@ -94,11 +93,13 @@ export const processVersionedRouter = (
|
|||
const hasBody = Boolean(extractValidationSchemaFromVersionedHandler(handler)?.request?.body);
|
||||
const contentType = extractContentType(route.options.options?.body);
|
||||
const hasVersionFilter = Boolean(filters?.version);
|
||||
// If any handler is deprecated we show deprecated: true in the spec
|
||||
const hasDeprecations = route.handlers.some(({ options }) => !!options.options?.deprecated);
|
||||
const operation: OpenAPIV3.OperationObject = {
|
||||
summary: route.options.summary ?? '',
|
||||
tags: route.options.options?.tags ? extractTags(route.options.options.tags) : [],
|
||||
...(route.options.description ? { description: route.options.description } : {}),
|
||||
...(route.options.deprecated ? { deprecated: route.options.deprecated } : {}),
|
||||
...(hasDeprecations ? { deprecated: true } : {}),
|
||||
...(route.options.discontinued ? { 'x-discontinued': route.options.discontinued } : {}),
|
||||
requestBody: hasBody
|
||||
? {
|
||||
|
|
|
@ -87,6 +87,7 @@ describe('request logging', () => {
|
|||
route: {
|
||||
method: 'get',
|
||||
path: '/',
|
||||
routePath: '/',
|
||||
options: expect.any(Object),
|
||||
},
|
||||
uuid: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx',
|
||||
|
@ -116,10 +117,12 @@ describe('request logging', () => {
|
|||
auth: { isAuthenticated: false },
|
||||
route: {
|
||||
path: '/',
|
||||
routePath: '/',
|
||||
method: 'get',
|
||||
options: {
|
||||
authRequired: true,
|
||||
xsrfRequired: false,
|
||||
deprecated: undefined,
|
||||
access: 'internal',
|
||||
tags: [],
|
||||
security: undefined,
|
||||
|
@ -127,7 +130,8 @@ describe('request logging', () => {
|
|||
body: undefined
|
||||
}
|
||||
},
|
||||
authzResult: undefined
|
||||
authzResult: undefined,
|
||||
apiVersion: undefined
|
||||
}"
|
||||
`);
|
||||
});
|
||||
|
|
|
@ -118,7 +118,6 @@ describe('createCounterFetcher', () => {
|
|||
dailyEvents,
|
||||
})
|
||||
);
|
||||
// @ts-expect-error incomplete mock implementation
|
||||
const { dailyEvents } = await fetch({ soClient: soClientMock });
|
||||
expect(dailyEvents).toHaveLength(5);
|
||||
const intersectingEntry = dailyEvents.find(
|
||||
|
|
|
@ -30,7 +30,7 @@ export function createCounterFetcher<T>(
|
|||
filter: string,
|
||||
transform: (counters: CounterEvent[]) => T
|
||||
) {
|
||||
return async ({ soClient }: CollectorFetchContext) => {
|
||||
return async ({ soClient }: Pick<CollectorFetchContext, 'soClient'>) => {
|
||||
const finder = soClient.createPointInTimeFinder<UsageCountersSavedObjectAttributes>({
|
||||
type: USAGE_COUNTERS_SAVED_OBJECT_TYPE,
|
||||
namespaces: ['*'],
|
||||
|
|
|
@ -0,0 +1,69 @@
|
|||
/*
|
||||
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import type { Logger } from '@kbn/logging';
|
||||
import type { CoreDeprecatedApiUsageStats } from '@kbn/core-usage-data-server';
|
||||
import { USAGE_COUNTERS_SAVED_OBJECT_TYPE } from '@kbn/usage-collection-plugin/server';
|
||||
|
||||
import { createCounterFetcher, type CounterEvent } from '../common/counters';
|
||||
|
||||
const DEPRECATED_API_COUNTERS_FILTER = `${USAGE_COUNTERS_SAVED_OBJECT_TYPE}.attributes.counterType: deprecated_api_call\\:*`;
|
||||
|
||||
const mergeCounter = (counter: CounterEvent, acc?: CoreDeprecatedApiUsageStats) => {
|
||||
if (acc && acc?.apiId !== counter.counterName) {
|
||||
throw new Error(
|
||||
`Failed to merge mismatching counterNames: ${acc.apiId} with ${counter.counterName}`
|
||||
);
|
||||
}
|
||||
const isMarkedCounter = counter.counterType.endsWith(':marked_as_resolved');
|
||||
|
||||
const finalCounter = {
|
||||
apiId: counter.counterName,
|
||||
apiTotalCalls: 0,
|
||||
apiLastCalledAt: 'unknown',
|
||||
totalMarkedAsResolved: 0,
|
||||
markedAsResolvedLastCalledAt: 'unknown',
|
||||
...(acc || {}),
|
||||
};
|
||||
|
||||
if (isMarkedCounter) {
|
||||
return finalCounter;
|
||||
}
|
||||
|
||||
const isResolvedCounter = counter.counterType.endsWith(':resolved');
|
||||
const totalKey = isResolvedCounter ? 'totalMarkedAsResolved' : 'apiTotalCalls';
|
||||
const lastUpdatedKey = isResolvedCounter ? 'markedAsResolvedLastCalledAt' : 'apiLastCalledAt';
|
||||
|
||||
const newPayload = {
|
||||
[totalKey]: (finalCounter[totalKey] || 0) + counter.total,
|
||||
[lastUpdatedKey]: counter.lastUpdatedAt,
|
||||
};
|
||||
|
||||
return {
|
||||
...finalCounter,
|
||||
...newPayload,
|
||||
};
|
||||
};
|
||||
|
||||
function mergeCounters(counters: CounterEvent[]): CoreDeprecatedApiUsageStats[] {
|
||||
const mergedCounters = counters.reduce((acc, counter) => {
|
||||
const { counterName } = counter;
|
||||
const existingCounter = acc[counterName];
|
||||
|
||||
acc[counterName] = mergeCounter(counter, existingCounter);
|
||||
|
||||
return acc;
|
||||
}, {} as Record<string, CoreDeprecatedApiUsageStats>);
|
||||
|
||||
return Object.values(mergedCounters);
|
||||
}
|
||||
|
||||
export const fetchDeprecatedApiCounterStats = (logger: Logger) => {
|
||||
return createCounterFetcher(logger, DEPRECATED_API_COUNTERS_FILTER, mergeCounters);
|
||||
};
|
|
@ -8,3 +8,4 @@
|
|||
*/
|
||||
|
||||
export { registerCoreUsageCollector } from './core_usage_collector';
|
||||
export { fetchDeprecatedApiCounterStats } from './fetch_deprecated_api_counters';
|
||||
|
|
|
@ -17,7 +17,7 @@ export {
|
|||
export { registerOpsStatsCollector } from './ops_stats';
|
||||
export { registerCloudProviderUsageCollector } from './cloud';
|
||||
export { registerCspCollector } from './csp';
|
||||
export { registerCoreUsageCollector } from './core';
|
||||
export { registerCoreUsageCollector, fetchDeprecatedApiCounterStats } from './core';
|
||||
export { registerLocalizationUsageCollector } from './localization';
|
||||
export { registerConfigUsageCollector } from './config_usage';
|
||||
export { registerUiCountersUsageCollector } from './ui_counters';
|
||||
|
|
|
@ -43,6 +43,7 @@ import {
|
|||
registerUsageCountersUsageCollector,
|
||||
registerSavedObjectsCountUsageCollector,
|
||||
registerEventLoopDelaysCollector,
|
||||
fetchDeprecatedApiCounterStats,
|
||||
} from './collectors';
|
||||
|
||||
interface KibanaUsageCollectionPluginsDepsSetup {
|
||||
|
@ -74,6 +75,10 @@ export class KibanaUsageCollectionPlugin implements Plugin {
|
|||
registerEbtCounters(coreSetup.analytics, usageCollection);
|
||||
this.eventLoopUsageCounter = usageCollection.createUsageCounter('eventLoop');
|
||||
coreSetup.coreUsageData.registerUsageCounter(usageCollection.createUsageCounter('core'));
|
||||
const deprecatedUsageFetch = fetchDeprecatedApiCounterStats(
|
||||
this.logger.get('deprecated-api-usage')
|
||||
);
|
||||
coreSetup.coreUsageData.registerDeprecatedUsageFetch(deprecatedUsageFetch);
|
||||
this.registerUsageCollectors(
|
||||
usageCollection,
|
||||
coreSetup,
|
||||
|
|
|
@ -151,7 +151,7 @@ export default function ({ getService, getPageObjects }: PluginFunctionalProvide
|
|||
});
|
||||
|
||||
expect(resolveResult).to.eql({
|
||||
reason: 'This deprecation cannot be resolved automatically.',
|
||||
reason: 'This deprecation cannot be resolved automatically or marked as resolved.',
|
||||
status: 'fail',
|
||||
});
|
||||
});
|
||||
|
|
|
@ -38,6 +38,7 @@ export const createActionRoute = (
|
|||
access: 'public',
|
||||
summary: `Create a connector`,
|
||||
tags: ['oas-tag:connectors'],
|
||||
// @ts-expect-error TODO(https://github.com/elastic/kibana/issues/196095): Replace {RouteDeprecationInfo}
|
||||
deprecated: true,
|
||||
},
|
||||
validate: {
|
||||
|
|
|
@ -32,6 +32,7 @@ export const deleteActionRoute = (
|
|||
summary: `Delete a connector`,
|
||||
description: 'WARNING: When you delete a connector, it cannot be recovered.',
|
||||
tags: ['oas-tag:connectors'],
|
||||
// @ts-expect-error TODO(https://github.com/elastic/kibana/issues/196095): Replace {RouteDeprecationInfo}
|
||||
deprecated: true,
|
||||
},
|
||||
validate: {
|
||||
|
|
|
@ -37,6 +37,7 @@ export const executeActionRoute = (
|
|||
options: {
|
||||
access: 'public',
|
||||
summary: `Run a connector`,
|
||||
// @ts-expect-error TODO(https://github.com/elastic/kibana/issues/196095): Replace {RouteDeprecationInfo}
|
||||
deprecated: true,
|
||||
tags: ['oas-tag:connectors'],
|
||||
},
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue