[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:
Ahmad Bamieh 2024-10-22 19:57:37 +03:00 committed by GitHub
parent 1e05086ea4
commit c417196905
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
139 changed files with 2057 additions and 381 deletions

View file

@ -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

View file

@ -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',

View file

@ -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',
};

View file

@ -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 {};
}

View file

@ -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);
}

View file

@ -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.' },
});
}
);
};

View file

@ -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')
);
};

View file

@ -8,3 +8,4 @@
*/
export { registerRoutes } from './register_routes';
export { registerDeprecatedRoutes } from './deprecated_routes';

View file

@ -20,5 +20,6 @@
"@kbn/core-http-browser",
"@kbn/config-schema",
"@kbn/react-kibana-context-render",
"@kbn/core-http-server",
]
}

View file

@ -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": [

View file

@ -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": [

View file

@ -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:

View file

@ -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:

View file

@ -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:

View file

@ -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:

View file

@ -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",
}
`);

View file

@ -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 {

View file

@ -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

View file

@ -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');
});
});

View file

@ -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 }),
});
};

View file

@ -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');
});
});

View file

@ -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,
})
);
},
});
}
};

View file

@ -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;
};

View file

@ -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';

View file

@ -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,
}));

View file

@ -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');
});
});
});

View file

@ -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,
})
);
},
});
}
}
}

View file

@ -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 });
}

View file

@ -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 });
}
};
}

View file

@ -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();
}
);
};

View file

@ -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/**/*",

View file

@ -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';

View file

@ -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,
};

View file

@ -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(
{

View file

@ -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) {

View file

@ -198,7 +198,7 @@ describe('Versioned route', () => {
expect(router.post).toHaveBeenCalledWith(
expect.objectContaining(expectedRouteConfig),
expect.any(Function),
{ isVersioned: true }
{ isVersioned: true, events: false }
);
});

View file

@ -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,

View file

@ -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",

View file

@ -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,
};
});
}

View file

@ -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';

View file

@ -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,

View file

@ -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.

View file

@ -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) {

View file

@ -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,

View file

@ -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.

View file

@ -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

View file

@ -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 */

View file

@ -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,

View file

@ -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';

View file

@ -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 */

View file

@ -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,

View file

@ -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.
*/

View file

@ -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

View file

@ -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;
}

View file

@ -19,4 +19,5 @@ export type {
VersionedRouter,
VersionedRouteCustomResponseBodyValidation,
VersionedResponseBodyValidation,
VersionedRouterRoute,
} from './types';

View file

@ -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;
}

View file

@ -15,7 +15,7 @@
"@kbn/utility-types",
"@kbn/core-base-common",
"@kbn/core-http-common",
"@kbn/zod"
"@kbn/zod",
],
"exclude": [
"target/**/*",

View file

@ -76,6 +76,8 @@ export function createCoreSetupMock({
userProfile: userProfileServiceMock.createSetup(),
coreUsageData: {
registerUsageCounter: coreUsageDataServiceMock.createSetupContract().registerUsageCounter,
registerDeprecatedUsageFetch:
coreUsageDataServiceMock.createSetupContract().registerDeprecatedUsageFetch,
},
plugins: {
onSetup: jest.fn(),

View file

@ -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),

View file

@ -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,

View file

@ -36,6 +36,7 @@ export const createCoreUsageDataSetupMock = () => {
getClient: jest.fn(),
registerUsageCounter: jest.fn(),
incrementUsageCounter: jest.fn(),
registerDeprecatedUsageFetch: jest.fn(),
};
return setupContract;
};

View file

@ -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: {

View file

@ -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: {

View file

@ -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: {

View file

@ -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.`,

View file

@ -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: {

View file

@ -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: {

View file

@ -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: {

View file

@ -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: {

View file

@ -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: {

View file

@ -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.`,

View file

@ -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: {

View file

@ -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>;

View file

@ -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;

View file

@ -52,6 +52,7 @@ describe('CoreUsageStatsClient', () => {
debugLogger: debugLoggerMock,
basePath: basePathMock,
repositoryPromise: Promise.resolve(repositoryMock),
fetchDeprecatedUsageStats: jest.fn(),
stop$,
incrementUsageCounter: incrementUsageCounterMock,
});

View file

@ -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);
}

View file

@ -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(),

View file

@ -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),

View file

@ -19,4 +19,6 @@ export type {
CoreConfigUsageData,
CoreServicesUsageData,
CoreUsageStats,
CoreDeprecatedApiUsageStats,
DeprecatedApiUsageFetcher,
} from './src';

View file

@ -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;
}

View file

@ -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';

View file

@ -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[]>;

View file

@ -12,5 +12,8 @@
],
"exclude": [
"target/**/*",
],
"kbn_references": [
"@kbn/core-saved-objects-api-server",
]
}

View file

@ -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 {

View file

@ -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: [
{

View file

@ -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:

View file

@ -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
? {

View file

@ -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,

View file

@ -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
? {

View file

@ -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
}"
`);
});

View file

@ -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(

View file

@ -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: ['*'],

View file

@ -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);
};

View file

@ -8,3 +8,4 @@
*/
export { registerCoreUsageCollector } from './core_usage_collector';
export { fetchDeprecatedApiCounterStats } from './fetch_deprecated_api_counters';

View file

@ -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';

View file

@ -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,

View file

@ -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',
});
});

View file

@ -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: {

View file

@ -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: {

View file

@ -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