diff --git a/.buildkite/ftr_platform_stateful_configs.yml b/.buildkite/ftr_platform_stateful_configs.yml index 41ba3fc0366d0..b015b1c96c73a 100644 --- a/.buildkite/ftr_platform_stateful_configs.yml +++ b/.buildkite/ftr_platform_stateful_configs.yml @@ -345,7 +345,7 @@ enabled: - x-pack/test/task_manager_claimer_update_by_query/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 diff --git a/.eslintrc.js b/.eslintrc.js index 006f39ce1026c..3c67594513c0e 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -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', diff --git a/examples/routing_example/common/index.ts b/examples/routing_example/common/index.ts index c86a994f469f1..b83582b66ff08 100644 --- a/examples/routing_example/common/index.ts +++ b/examples/routing_example/common/index.ts @@ -15,3 +15,9 @@ export const POST_MESSAGE_ROUTE_PATH = '/api/post_message'; // Internal APIs should use the `internal` prefix, instead of the `api` prefix. export const INTERNAL_GET_MESSAGE_BY_ID_ROUTE = '/internal/get_message'; + +export const DEPRECATED_ROUTES = { + REMOVED_ROUTE: '/api/routing_example/d/removed_route', + MIGRATED_ROUTE: '/api/routing_example/d/migrated_route', + VERSIONED_ROUTE: '/api/routing_example/d/versioned', +}; diff --git a/examples/routing_example/server/plugin.ts b/examples/routing_example/server/plugin.ts index cb6a00920dd05..d4036afc58b5b 100644 --- a/examples/routing_example/server/plugin.ts +++ b/examples/routing_example/server/plugin.ts @@ -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 {}; } diff --git a/examples/routing_example/server/routes/deprecated_routes/index.ts b/examples/routing_example/server/routes/deprecated_routes/index.ts new file mode 100644 index 0000000000000..75dc0261ed1b9 --- /dev/null +++ b/examples/routing_example/server/routes/deprecated_routes/index.ts @@ -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); +} diff --git a/examples/routing_example/server/routes/deprecated_routes/unversioned.ts b/examples/routing_example/server/routes/deprecated_routes/unversioned.ts new file mode 100644 index 0000000000000..4e1451a91fc38 --- /dev/null +++ b/examples/routing_example/server/routes/deprecated_routes/unversioned.ts @@ -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.' }, + }); + } + ); +}; diff --git a/examples/routing_example/server/routes/deprecated_routes/versioned.ts b/examples/routing_example/server/routes/deprecated_routes/versioned.ts new file mode 100644 index 0000000000000..54d6f779f77c3 --- /dev/null +++ b/examples/routing_example/server/routes/deprecated_routes/versioned.ts @@ -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') + ); +}; diff --git a/examples/routing_example/server/routes/index.ts b/examples/routing_example/server/routes/index.ts index 2f43c85b0d471..1c1ccb9e191ff 100644 --- a/examples/routing_example/server/routes/index.ts +++ b/examples/routing_example/server/routes/index.ts @@ -8,3 +8,4 @@ */ export { registerRoutes } from './register_routes'; +export { registerDeprecatedRoutes } from './deprecated_routes'; diff --git a/examples/routing_example/tsconfig.json b/examples/routing_example/tsconfig.json index b35e8dbd34f4a..86bfda9d3d529 100644 --- a/examples/routing_example/tsconfig.json +++ b/examples/routing_example/tsconfig.json @@ -20,5 +20,6 @@ "@kbn/core-http-browser", "@kbn/config-schema", "@kbn/react-kibana-context-render", + "@kbn/core-http-server", ] } diff --git a/packages/core/deprecations/core-deprecations-browser-internal/src/deprecations_client.test.ts b/packages/core/deprecations/core-deprecations-browser-internal/src/deprecations_client.test.ts index bfb4f35fa93f1..38777474f136b 100644 --- a/packages/core/deprecations/core-deprecations-browser-internal/src/deprecations_client.test.ts +++ b/packages/core/deprecations/core-deprecations-browser-internal/src/deprecations_client.test.ts @@ -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", } `); diff --git a/packages/core/deprecations/core-deprecations-browser-internal/src/deprecations_client.ts b/packages/core/deprecations/core-deprecations-browser-internal/src/deprecations_client.ts index f1f473d726b7e..bdcad406995a9 100644 --- a/packages/core/deprecations/core-deprecations-browser-internal/src/deprecations_client.ts +++ b/packages/core/deprecations/core-deprecations-browser-internal/src/deprecations_client.ts @@ -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 => { + ): 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({ + 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 => { + 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(fetchParams); return { status: 'ok' }; } catch (err) { return { diff --git a/packages/core/deprecations/core-deprecations-common/src/types.ts b/packages/core/deprecations/core-deprecations-common/src/types.ts index bf9a4a673d721..85da30b2c1287 100644 --- a/packages/core/deprecations/core-deprecations-common/src/types.ts +++ b/packages/core/deprecations/core-deprecations-common/src/types.ts @@ -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 diff --git a/packages/core/deprecations/core-deprecations-server-internal/src/deprecations/api_deprecations.test.ts b/packages/core/deprecations/core-deprecations-server-internal/src/deprecations/api_deprecations.test.ts new file mode 100644 index 0000000000000..b431088152f3e --- /dev/null +++ b/packages/core/deprecations/core-deprecations-server-internal/src/deprecations/api_deprecations.test.ts @@ -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; + let http: ReturnType; + let coreUsageData: ReturnType; + + 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 => + _.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 => + _.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'); + }); +}); diff --git a/packages/core/deprecations/core-deprecations-server-internal/src/deprecations/api_deprecations.ts b/packages/core/deprecations/core-deprecations-server-internal/src/deprecations/api_deprecations.ts new file mode 100644 index 0000000000000..45893987ddf92 --- /dev/null +++ b/packages/core/deprecations/core-deprecations-server-internal/src/deprecations/api_deprecations.ts @@ -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): string => { + return [ + routeVersion || 'unversioned', + routeMethod.toLocaleLowerCase(), + routePath.replace(/\/$/, ''), + ].join('|'); +}; + +export const createGetApiDeprecations = + ({ http, coreUsageData }: Pick) => + async (): Promise => { + 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 }), + }); +}; diff --git a/packages/core/deprecations/core-deprecations-server-internal/src/deprecations/config_deprecations.test.ts b/packages/core/deprecations/core-deprecations-server-internal/src/deprecations/config_deprecations.test.ts new file mode 100644 index 0000000000000..92d0703c8037e --- /dev/null +++ b/packages/core/deprecations/core-deprecations-server-internal/src/deprecations/config_deprecations.test.ts @@ -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; + + 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'); + }); +}); diff --git a/packages/core/deprecations/core-deprecations-server-internal/src/deprecations/config_deprecations.ts b/packages/core/deprecations/core-deprecations-server-internal/src/deprecations/config_deprecations.ts new file mode 100644 index 0000000000000..f9df0edacd9d1 --- /dev/null +++ b/packages/core/deprecations/core-deprecations-server-internal/src/deprecations/config_deprecations.ts @@ -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, + }) + ); + }, + }); + } +}; diff --git a/packages/core/deprecations/core-deprecations-server-internal/src/deprecations/i18n_texts.ts b/packages/core/deprecations/core-deprecations-server-internal/src/deprecations/i18n_texts.ts new file mode 100644 index 0000000000000..cb1dacc97bd91 --- /dev/null +++ b/packages/core/deprecations/core-deprecations-server-internal/src/deprecations/i18n_texts.ts @@ -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; +}; diff --git a/packages/core/deprecations/core-deprecations-server-internal/src/deprecations/index.ts b/packages/core/deprecations/core-deprecations-server-internal/src/deprecations/index.ts new file mode 100644 index 0000000000000..aecf3d5b299a2 --- /dev/null +++ b/packages/core/deprecations/core-deprecations-server-internal/src/deprecations/index.ts @@ -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'; diff --git a/packages/core/deprecations/core-deprecations-server-internal/src/deprecations_service.test.mocks.ts b/packages/core/deprecations/core-deprecations-server-internal/src/deprecations_service.test.mocks.ts index 9ce9f52fb7a50..93550539343e3 100644 --- a/packages/core/deprecations/core-deprecations-server-internal/src/deprecations_service.test.mocks.ts +++ b/packages/core/deprecations/core-deprecations-server-internal/src/deprecations_service.test.mocks.ts @@ -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, })); diff --git a/packages/core/deprecations/core-deprecations-server-internal/src/deprecations_service.test.ts b/packages/core/deprecations/core-deprecations-server-internal/src/deprecations_service.test.ts index 14a131ca8e563..39c299d980531 100644 --- a/packages/core/deprecations/core-deprecations-server-internal/src/deprecations_service.test.ts +++ b/packages/core/deprecations/core-deprecations-server-internal/src/deprecations_service.test.ts @@ -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; let http: ReturnType; let router: ReturnType; let deprecationsCoreSetupDeps: DeprecationsSetupDeps; + let coreUsageData: ReturnType; 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'); - }); - }); }); diff --git a/packages/core/deprecations/core-deprecations-server-internal/src/deprecations_service.ts b/packages/core/deprecations/core-deprecations-server-internal/src/deprecations_service.ts index 4c8f564943ab1..c0a0ef0f88c7b 100644 --- a/packages/core/deprecations/core-deprecations-server-internal/src/deprecations_service.ts +++ b/packages/core/deprecations/core-deprecations-server-internal/src/deprecations_service.ts @@ -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 { + public async setup({ + http, + coreUsageData, + }: DeprecationsSetupDeps): Promise { 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, - }) - ); - }, - }); - } - } } diff --git a/packages/core/deprecations/core-deprecations-server-internal/src/routes/index.ts b/packages/core/deprecations/core-deprecations-server-internal/src/routes/index.ts index e1a925610327f..f812bbfd15acd 100644 --- a/packages/core/deprecations/core-deprecations-server-internal/src/routes/index.ts +++ b/packages/core/deprecations/core-deprecations-server-internal/src/routes/index.ts @@ -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('/api/deprecations'); registerGetRoute(router); + + registerApiDeprecationsPostValidationHandler({ http, coreUsageData }); + registerMarkAsResolvedRoute(router, { coreUsageData }); } diff --git a/packages/core/deprecations/core-deprecations-server-internal/src/routes/post_validation_handler.ts b/packages/core/deprecations/core-deprecations-server-internal/src/routes/post_validation_handler.ts new file mode 100644 index 0000000000000..b93c17af2f536 --- /dev/null +++ b/packages/core/deprecations/core-deprecations-server-internal/src/routes/post_validation_handler.ts @@ -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 }); + } + }; +} diff --git a/packages/core/deprecations/core-deprecations-server-internal/src/routes/resolve_deprecated_api.ts b/packages/core/deprecations/core-deprecations-server-internal/src/routes/resolve_deprecated_api.ts new file mode 100644 index 0000000000000..840bc5ac22d23 --- /dev/null +++ b/packages/core/deprecations/core-deprecations-server-internal/src/routes/resolve_deprecated_api.ts @@ -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(); + } + ); +}; diff --git a/packages/core/deprecations/core-deprecations-server-internal/tsconfig.json b/packages/core/deprecations/core-deprecations-server-internal/tsconfig.json index ba06a3e9ec2f7..02be6b7cb8198 100644 --- a/packages/core/deprecations/core-deprecations-server-internal/tsconfig.json +++ b/packages/core/deprecations/core-deprecations-server-internal/tsconfig.json @@ -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/**/*", diff --git a/packages/core/http/core-http-router-server-internal/index.ts b/packages/core/http/core-http-router-server-internal/index.ts index 6c684d5f8169c..6aa6ac117f533 100644 --- a/packages/core/http/core-http-router-server-internal/index.ts +++ b/packages/core/http/core-http-router-server-internal/index.ts @@ -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'; diff --git a/packages/core/http/core-http-router-server-internal/src/request.ts b/packages/core/http/core-http-router-server-internal/src/request.ts index 286b900fc24f5..9f89f1a70bb47 100644 --- a/packages/core/http/core-http-router-server-internal/src/request.ts +++ b/packages/core/http/core-http-router-server-internal/src/request.ts @@ -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, }; diff --git a/packages/core/http/core-http-router-server-internal/src/router.test.ts b/packages/core/http/core-http-router-server-internal/src/router.test.ts index f611e3b6308fe..2c702fb3ef702 100644 --- a/packages/core/http/core-http-router-server-internal/src/router.test.ts +++ b/packages/core/http/core-http-router-server-internal/src/router.test.ts @@ -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( { diff --git a/packages/core/http/core-http-router-server-internal/src/router.ts b/packages/core/http/core-http-router-server-internal/src/router.ts index 98559c1be636f..a686ad1671dc7 100644 --- a/packages/core/http/core-http-router-server-internal/src/router.ts +++ b/packages/core/http/core-http-router-server-internal/src/router.ts @@ -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) => RequestHandlerEnhanced; -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 ReturnType>; /** @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 implements IRouter { - public routes: Array> = []; + private static ee = new EventEmitter(); + public routes: Array> = []; public pluginId?: symbol; public get: InternalRegistrar<'get', Context>; public post: InternalRegistrar<'post', Context>; @@ -202,25 +205,27 @@ export class Router( route: InternalRouteConfig, handler: RequestHandler, - { 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, 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 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 { + const postValidate: RouterEvents = 'onPostValidate'; + Router.ee.emit(postValidate, request, routeOptions); + }; + private async handle({ 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, @@ -305,18 +333,24 @@ export class Router { expect(router.post).toHaveBeenCalledWith( expect.objectContaining(expectedRouteConfig), expect.any(Function), - { isVersioned: true } + { isVersioned: true, events: false } ); }); diff --git a/packages/core/http/core-http-router-server-internal/src/versioned_router/core_versioned_route.ts b/packages/core/http/core-http-router-server-internal/src/versioned_router/core_versioned_route.ts index e9a9e60de8193..45654696ba0cf 100644 --- a/packages/core/http/core-http-router-server-internal/src/versioned_router/core_versioned_route.ts +++ b/packages/core/http/core-http-router-server-internal/src/versioned_router/core_versioned_route.ts @@ -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; - interface InternalVersionedRouteConfig extends VersionedRouteConfig { isDev: boolean; useVersionResolutionStrategyForInternalPaths: Map; @@ -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; 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): VersionedRoute { this.validateVersion(options.version); options = prepareVersionedRouteValidation(options); - this.handlers.set(options.version, { fn: handler, options, diff --git a/packages/core/http/core-http-router-server-internal/src/versioned_router/core_versioned_router.test.ts b/packages/core/http/core-http-router-server-internal/src/versioned_router/core_versioned_router.test.ts index d56de36ba9a29..a3ffffc0ef219 100644 --- a/packages/core/http/core-http-router-server-internal/src/versioned_router/core_versioned_router.test.ts +++ b/packages/core/http/core-http-router-server-internal/src/versioned_router/core_versioned_router.test.ts @@ -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", diff --git a/packages/core/http/core-http-router-server-internal/src/versioned_router/core_versioned_router.ts b/packages/core/http/core-http-router-server-internal/src/versioned_router/core_versioned_router.ts index e9272e17ab18e..ef1f8255420ae 100644 --- a/packages/core/http/core-http-router-server-internal/src/versioned_router/core_versioned_router.ts +++ b/packages/core/http/core-http-router-server-internal/src/versioned_router/core_versioned_router.ts @@ -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, }; }); } diff --git a/packages/core/http/core-http-router-server-internal/src/versioned_router/index.ts b/packages/core/http/core-http-router-server-internal/src/versioned_router/index.ts index e283fcc2a590f..14c08076faae0 100644 --- a/packages/core/http/core-http-router-server-internal/src/versioned_router/index.ts +++ b/packages/core/http/core-http-router-server-internal/src/versioned_router/index.ts @@ -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'; diff --git a/packages/core/http/core-http-router-server-internal/src/versioned_router/mocks.ts b/packages/core/http/core-http-router-server-internal/src/versioned_router/mocks.ts index 5a958fa9251f7..36a672ca6a9f7 100644 --- a/packages/core/http/core-http-router-server-internal/src/versioned_router/mocks.ts +++ b/packages/core/http/core-http-router-server-internal/src/versioned_router/mocks.ts @@ -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, diff --git a/packages/core/http/core-http-router-server-internal/src/versioned_router/types.ts b/packages/core/http/core-http-router-server-internal/src/versioned_router/types.ts index aec1f8b0cf0ab..bdcaae486cd9c 100644 --- a/packages/core/http/core-http-router-server-internal/src/versioned_router/types.ts +++ b/packages/core/http/core-http-router-server-internal/src/versioned_router/types.ts @@ -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; /** @internal */ -export interface VersionedRouterRoute { - method: string; - path: string; - options: Omit, 'path'>; - handlers: Array<{ - fn: RequestHandler; - options: AddVersionOpts; - }>; -} +export type Options = AddVersionOpts; /** * Specifies resolution strategy to use if a request does not provide a version. diff --git a/packages/core/http/core-http-router-server-mocks/src/versioned_router.mock.ts b/packages/core/http/core-http-router-server-mocks/src/versioned_router.mock.ts index 987288cf372bd..fec80b06963a0 100644 --- a/packages/core/http/core-http-router-server-mocks/src/versioned_router.mock.ts +++ b/packages/core/http/core-http-router-server-mocks/src/versioned_router.mock.ts @@ -14,6 +14,7 @@ import type { AddVersionOpts, RequestHandler, KibanaResponseFactory, + VersionedRouterRoute, } from '@kbn/core-http-server'; export type MockedVersionedRoute = jest.Mocked; @@ -24,14 +25,16 @@ const createMockVersionedRoute = (): MockedVersionedRoute => { return api; }; +type VersionedRouterMethods = keyof Omit; + export type MockedVersionedRouter = jest.Mocked> & { - 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 = { + const router: Omit = { 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, - method: keyof VersionedRouter, + router: Omit, + method: VersionedRouterMethods, path: string ): RegisteredVersionedRoute => { if (!router[method].mock.calls.length) { diff --git a/packages/core/http/core-http-server-internal/src/http_server.test.ts b/packages/core/http/core-http-server-internal/src/http_server.test.ts index c374ff7ca2107..69e69f784e65e 100644 --- a/packages/core/http/core-http-server-internal/src/http_server.test.ts +++ b/packages/core/http/core-http-server-internal/src/http_server.test.ts @@ -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, diff --git a/packages/core/http/core-http-server-internal/src/http_server.ts b/packages/core/http/core-http-server-internal/src/http_server.ts index 46470bac7c504..1dee6a3286788 100644 --- a/packages/core/http/core-http-server-internal/src/http_server.ts +++ b/packages/core/http/core-http-server-internal/src/http_server.ts @@ -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. diff --git a/packages/core/http/core-http-server-internal/src/http_service.ts b/packages/core/http/core-http-server-internal/src/http_service.ts index 3f803b06f15fd..af310a6792057 100644 --- a/packages/core/http/core-http-server-internal/src/http_service.ts +++ b/packages/core/http/core-http-server-internal/src/http_service.ts @@ -162,7 +162,7 @@ export class HttpService return this.internalPreboot; } - public async setup(deps: SetupDeps) { + public async setup(deps: SetupDeps): Promise { 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: ( path: string, pluginId: PluginOpaqueId = this.coreContext.coreId diff --git a/packages/core/http/core-http-server-internal/src/types.ts b/packages/core/http/core-http-server-internal/src/types.ts index 70dde23f035d0..0706af9ad73a2 100644 --- a/packages/core/http/core-http-server-internal/src/types.ts +++ b/packages/core/http/core-http-server-internal/src/types.ts @@ -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; + 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 ) => IContextContainer; + getRegisteredDeprecatedApis: () => RouterDeprecatedRouteDetails[]; } /** @internal */ diff --git a/packages/core/http/core-http-server-mocks/src/http_service.mock.ts b/packages/core/http/core-http-server-mocks/src/http_service.mock.ts index 4e803ee5f86a8..116db3648f120 100644 --- a/packages/core/http/core-http-server-mocks/src/http_service.mock.ts +++ b/packages/core/http/core-http-server-mocks/src/http_service.mock.ts @@ -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, diff --git a/packages/core/http/core-http-server/index.ts b/packages/core/http/core-http-server/index.ts index f7cae6e02165e..9c12f6a09ac45 100644 --- a/packages/core/http/core-http-server/index.ts +++ b/packages/core/http/core-http-server/index.ts @@ -93,6 +93,7 @@ export type { IRouter, RouteRegistrar, RouterRoute, + RouterDeprecatedRouteDetails, IKibanaSocket, KibanaErrorResponseFactory, KibanaRedirectionResponseFactory, @@ -171,6 +172,7 @@ export type { VersionedRouter, VersionedRouteCustomResponseBodyValidation, VersionedResponseBodyValidation, + VersionedRouterRoute, } from './src/versioning'; export type { IStaticAssets } from './src/static_assets'; diff --git a/packages/core/http/core-http-server/src/http_contract.ts b/packages/core/http/core-http-server/src/http_contract.ts index 72eb70149f529..e2f675bd8d0c0 100644 --- a/packages/core/http/core-http-server/src/http_contract.ts +++ b/packages/core/http/core-http-server/src/http_contract.ts @@ -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 */ diff --git a/packages/core/http/core-http-server/src/router/index.ts b/packages/core/http/core-http-server/src/router/index.ts index 7e4be0507ae1a..8e2b9373c43bd 100644 --- a/packages/core/http/core-http-server/src/router/index.ts +++ b/packages/core/http/core-http-server/src/router/index.ts @@ -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, diff --git a/packages/core/http/core-http-server/src/router/request.ts b/packages/core/http/core-http-server/src/router/request.ts index 5cb84a21be0c3..066372faca1e4 100644 --- a/packages/core/http/core-http-server/src/router/request.ts +++ b/packages/core/http/core-http-server/src/router/request.ts @@ -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 { path: string; method: Method; options: KibanaRequestRouteOptions; + 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. */ diff --git a/packages/core/http/core-http-server/src/router/route.ts b/packages/core/http/core-http-server/src/router/route.ts index 635c67f4b3d18..17fecd1c48b17 100644 --- a/packages/core/http/core-http-server/src/router/route.ts +++ b/packages/core/http/core-http-server/src/router/route.ts @@ -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. * @@ -285,12 +322,18 @@ export interface RouteConfigOptions { 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 diff --git a/packages/core/http/core-http-server/src/router/router.ts b/packages/core/http/core-http-server/src/router/router.ts index ba2b5eb906a93..d8b79bee13025 100644 --- a/packages/core/http/core-http-server/src/router/router.ts +++ b/packages/core/http/core-http-server/src/router/router.ts @@ -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 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>; + isVersioned: false; +} + +/** @public */ +export interface RouterDeprecatedRouteDetails { + routeDeprecationOptions: RouteDeprecationInfo; + routeMethod: RouteMethod; + routePath: string; + routeVersion?: string; } diff --git a/packages/core/http/core-http-server/src/versioning/index.ts b/packages/core/http/core-http-server/src/versioning/index.ts index 94b60bd105aac..8d8a664c769ac 100644 --- a/packages/core/http/core-http-server/src/versioning/index.ts +++ b/packages/core/http/core-http-server/src/versioning/index.ts @@ -19,4 +19,5 @@ export type { VersionedRouter, VersionedRouteCustomResponseBodyValidation, VersionedResponseBodyValidation, + VersionedRouterRoute, } from './types'; diff --git a/packages/core/http/core-http-server/src/versioning/types.ts b/packages/core/http/core-http-server/src/versioning/types.ts index 7998c9cc91fa9..63e1e37754803 100644 --- a/packages/core/http/core-http-server/src/versioning/types.ts +++ b/packages/core/http/core-http-server/src/versioning/types.ts @@ -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 = 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 { * @track-adoption */ delete: VersionedRouteRegistrar<'delete', Ctx>; + + /** + * @public + */ + getRoutes: () => VersionedRouterRoute[]; } /** @public */ @@ -341,6 +338,10 @@ export interface AddVersionOpts { validate: false | VersionedRouteValidation | (() => VersionedRouteValidation); // Provide a way to lazily load validation schemas security?: Exclude['security'], undefined>; + + options?: { + deprecated?: RouteDeprecationInfo; + }; } /** @@ -363,3 +364,11 @@ export interface VersionedRoute< handler: (...params: Parameters>) => MaybePromise ): VersionedRoute; } + +export interface VersionedRouterRoute

{ + method: string; + path: string; + options: Omit, 'path'>; + handlers: Array<{ fn: RequestHandler; options: AddVersionOpts }>; + isVersioned: true; +} diff --git a/packages/core/http/core-http-server/tsconfig.json b/packages/core/http/core-http-server/tsconfig.json index 64b2dacf2f292..50a0ce973eb4e 100644 --- a/packages/core/http/core-http-server/tsconfig.json +++ b/packages/core/http/core-http-server/tsconfig.json @@ -15,7 +15,7 @@ "@kbn/utility-types", "@kbn/core-base-common", "@kbn/core-http-common", - "@kbn/zod" + "@kbn/zod", ], "exclude": [ "target/**/*", diff --git a/packages/core/lifecycle/core-lifecycle-server-mocks/src/core_setup.mock.ts b/packages/core/lifecycle/core-lifecycle-server-mocks/src/core_setup.mock.ts index b50e9279d4721..30f5958bd92c5 100644 --- a/packages/core/lifecycle/core-lifecycle-server-mocks/src/core_setup.mock.ts +++ b/packages/core/lifecycle/core-lifecycle-server-mocks/src/core_setup.mock.ts @@ -76,6 +76,8 @@ export function createCoreSetupMock({ userProfile: userProfileServiceMock.createSetup(), coreUsageData: { registerUsageCounter: coreUsageDataServiceMock.createSetupContract().registerUsageCounter, + registerDeprecatedUsageFetch: + coreUsageDataServiceMock.createSetupContract().registerDeprecatedUsageFetch, }, plugins: { onSetup: jest.fn(), diff --git a/packages/core/plugins/core-plugins-server-internal/src/plugin_context.ts b/packages/core/plugins/core-plugins-server-internal/src/plugin_context.ts index 2fcdf384cb897..d7d40c9b792f7 100644 --- a/packages/core/plugins/core-plugins-server-internal/src/plugin_context.ts +++ b/packages/core/plugins/core-plugins-server-internal/src/plugin_context.ts @@ -225,6 +225,7 @@ export function createPluginSetupContext({ }, http: { createCookieSessionStorageFactory: deps.http.createCookieSessionStorageFactory, + getDeprecatedRoutes: deps.http.getDeprecatedRoutes, registerRouteHandlerContext: < Context extends RequestHandlerContext, ContextName extends keyof Omit @@ -283,6 +284,7 @@ export function createPluginSetupContext({ deprecations: deps.deprecations.getRegistry(plugin.name), coreUsageData: { registerUsageCounter: deps.coreUsageData.registerUsageCounter, + registerDeprecatedUsageFetch: deps.coreUsageData.registerDeprecatedUsageFetch, }, plugins: { onSetup: (...dependencyNames) => runtimeResolver.onSetup(plugin.name, dependencyNames), diff --git a/packages/core/root/core-root-server-internal/src/server.ts b/packages/core/root/core-root-server-internal/src/server.ts index 447db192c3048..5082a27930e87 100644 --- a/packages/core/root/core-root-server-internal/src/server.ts +++ b/packages/core/root/core-root-server-internal/src/server.ts @@ -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, diff --git a/packages/core/saved-objects/core-saved-objects-server-internal/src/mocks/internal_mocks.ts b/packages/core/saved-objects/core-saved-objects-server-internal/src/mocks/internal_mocks.ts index 625c8ed77fb48..ff5fe86df1173 100644 --- a/packages/core/saved-objects/core-saved-objects-server-internal/src/mocks/internal_mocks.ts +++ b/packages/core/saved-objects/core-saved-objects-server-internal/src/mocks/internal_mocks.ts @@ -36,6 +36,7 @@ export const createCoreUsageDataSetupMock = () => { getClient: jest.fn(), registerUsageCounter: jest.fn(), incrementUsageCounter: jest.fn(), + registerDeprecatedUsageFetch: jest.fn(), }; return setupContract; }; diff --git a/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/bulk_create.ts b/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/bulk_create.ts index b90aa0226d71c..c9a8656b3f753 100644 --- a/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/bulk_create.ts +++ b/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/bulk_create.ts @@ -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: { diff --git a/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/bulk_delete.ts b/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/bulk_delete.ts index 0f33ddc384bed..65209a6072748 100644 --- a/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/bulk_delete.ts +++ b/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/bulk_delete.ts @@ -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: { diff --git a/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/bulk_get.ts b/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/bulk_get.ts index 95fd9f5eab10a..3f87ca12248ae 100644 --- a/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/bulk_get.ts +++ b/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/bulk_get.ts @@ -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: { diff --git a/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/bulk_resolve.ts b/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/bulk_resolve.ts index d6b74131fb74d..8e19114e798e0 100644 --- a/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/bulk_resolve.ts +++ b/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/bulk_resolve.ts @@ -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.`, diff --git a/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/bulk_update.ts b/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/bulk_update.ts index 7a7ec340d98ca..825a5f95482c0 100644 --- a/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/bulk_update.ts +++ b/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/bulk_update.ts @@ -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: { diff --git a/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/create.ts b/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/create.ts index c8bfd4c0feaf9..57f4a10ed9377 100644 --- a/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/create.ts +++ b/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/create.ts @@ -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: { diff --git a/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/delete.ts b/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/delete.ts index 7ef8aac3fa1b1..69287821d8049 100644 --- a/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/delete.ts +++ b/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/delete.ts @@ -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: { diff --git a/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/find.ts b/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/find.ts index ac3b0555a7694..884ba1ed5c423 100644 --- a/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/find.ts +++ b/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/find.ts @@ -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: { diff --git a/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/get.ts b/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/get.ts index 9784ef1c79ff4..9fe3aa8ff20c7 100644 --- a/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/get.ts +++ b/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/get.ts @@ -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: { diff --git a/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/resolve.ts b/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/resolve.ts index 295acacc0ba0e..28a6c82e9ffdf 100644 --- a/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/resolve.ts +++ b/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/resolve.ts @@ -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.`, diff --git a/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/update.ts b/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/update.ts index 9eeea40a29b68..cfedc3ce03d2a 100644 --- a/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/update.ts +++ b/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/update.ts @@ -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: { diff --git a/packages/core/usage-data/core-usage-data-base-server-internal/src/usage_stats_client.ts b/packages/core/usage-data/core-usage-data-base-server-internal/src/usage_stats_client.ts index 649e972af2abc..044fb683fb69a 100644 --- a/packages/core/usage-data/core-usage-data-base-server-internal/src/usage_stats_client.ts +++ b/packages/core/usage-data/core-usage-data-base-server-internal/src/usage_stats_client.ts @@ -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; + getDeprecatedApiUsageStats(): Promise; + + incrementDeprecatedApi( + counterName: string, + options: { resolved?: boolean; incrementBy?: number } + ): Promise; + incrementSavedObjectsBulkCreate(options: BaseIncrementOptions): Promise; incrementSavedObjectsBulkGet(options: BaseIncrementOptions): Promise; diff --git a/packages/core/usage-data/core-usage-data-server-internal/src/core_usage_data_service.ts b/packages/core/usage-data/core-usage-data-server-internal/src/core_usage_data_service.ts index 2a10e06567d02..7ac7cbb7fbb57 100644 --- a/packages/core/usage-data/core-usage-data-server-internal/src/core_usage_data_service.ts +++ b/packages/core/usage-data/core-usage-data-server-internal/src/core_usage_data_service.ts @@ -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; diff --git a/packages/core/usage-data/core-usage-data-server-internal/src/core_usage_stats_client.test.ts b/packages/core/usage-data/core-usage-data-server-internal/src/core_usage_stats_client.test.ts index 948332c71f59a..9702e4b512345 100644 --- a/packages/core/usage-data/core-usage-data-server-internal/src/core_usage_stats_client.test.ts +++ b/packages/core/usage-data/core-usage-data-server-internal/src/core_usage_stats_client.test.ts @@ -52,6 +52,7 @@ describe('CoreUsageStatsClient', () => { debugLogger: debugLoggerMock, basePath: basePathMock, repositoryPromise: Promise.resolve(repositoryMock), + fetchDeprecatedUsageStats: jest.fn(), stop$, incrementUsageCounter: incrementUsageCounterMock, }); diff --git a/packages/core/usage-data/core-usage-data-server-internal/src/core_usage_stats_client.ts b/packages/core/usage-data/core-usage-data-server-internal/src/core_usage_stats_client.ts index 67ab6d9b30c9c..69eba9e1abf23 100644 --- a/packages/core/usage-data/core-usage-data-server-internal/src/core_usage_stats_client.ts +++ b/packages/core/usage-data/core-usage-data-server-internal/src/core_usage_stats_client.ts @@ -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; incrementUsageCounter: (params: CoreIncrementCounterParams) => void; bufferTimeMs?: number; + fetchDeprecatedUsageStats: DeprecatedApiUsageFetcher; } /** @internal */ @@ -126,6 +138,8 @@ export class CoreUsageStatsClient implements ICoreUsageStatsClient { private readonly fieldsToIncrement$ = new Subject(); private readonly flush$ = new Subject(); private readonly coreUsageEvents$ = new Subject(); + private readonly coreUsageDeprecatedApiCalls$ = new Subject(); + 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); } diff --git a/packages/core/usage-data/core-usage-data-server-mocks/src/core_usage_data_service.mock.ts b/packages/core/usage-data/core-usage-data-server-mocks/src/core_usage_data_service.mock.ts index c85aee50653d2..57628901c1a60 100644 --- a/packages/core/usage-data/core-usage-data-server-mocks/src/core_usage_data_service.mock.ts +++ b/packages/core/usage-data/core-usage-data-server-mocks/src/core_usage_data_service.mock.ts @@ -17,6 +17,7 @@ import { coreUsageStatsClientMock } from './core_usage_stats_client.mock'; const createSetupContractMock = (usageStatsClient = coreUsageStatsClientMock.create()) => { const setupContract: jest.Mocked = { registerType: jest.fn(), + registerDeprecatedUsageFetch: jest.fn(), getClient: jest.fn().mockReturnValue(usageStatsClient), registerUsageCounter: jest.fn(), incrementUsageCounter: jest.fn(), diff --git a/packages/core/usage-data/core-usage-data-server-mocks/src/core_usage_stats_client.mock.ts b/packages/core/usage-data/core-usage-data-server-mocks/src/core_usage_stats_client.mock.ts index f6b11204ca040..8896e9066ef78 100644 --- a/packages/core/usage-data/core-usage-data-server-mocks/src/core_usage_stats_client.mock.ts +++ b/packages/core/usage-data/core-usage-data-server-mocks/src/core_usage_stats_client.mock.ts @@ -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), diff --git a/packages/core/usage-data/core-usage-data-server/index.ts b/packages/core/usage-data/core-usage-data-server/index.ts index 77fb0b1066750..45ed369ca8381 100644 --- a/packages/core/usage-data/core-usage-data-server/index.ts +++ b/packages/core/usage-data/core-usage-data-server/index.ts @@ -19,4 +19,6 @@ export type { CoreConfigUsageData, CoreServicesUsageData, CoreUsageStats, + CoreDeprecatedApiUsageStats, + DeprecatedApiUsageFetcher, } from './src'; diff --git a/packages/core/usage-data/core-usage-data-server/src/core_usage_stats.ts b/packages/core/usage-data/core-usage-data-server/src/core_usage_stats.ts index 36409a097129c..39df9d30d19c9 100644 --- a/packages/core/usage-data/core-usage-data-server/src/core_usage_stats.ts +++ b/packages/core/usage-data/core-usage-data-server/src/core_usage_stats.ts @@ -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; +} diff --git a/packages/core/usage-data/core-usage-data-server/src/index.ts b/packages/core/usage-data/core-usage-data-server/src/index.ts index 01cd52adbe986..05d0f02500053 100644 --- a/packages/core/usage-data/core-usage-data-server/src/index.ts +++ b/packages/core/usage-data/core-usage-data-server/src/index.ts @@ -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'; diff --git a/packages/core/usage-data/core-usage-data-server/src/setup_contract.ts b/packages/core/usage-data/core-usage-data-server/src/setup_contract.ts index bd87563792e6d..30ed7edb6ce1d 100644 --- a/packages/core/usage-data/core-usage-data-server/src/setup_contract.ts +++ b/packages/core/usage-data/core-usage-data-server/src/setup_contract.ts @@ -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; diff --git a/packages/core/usage-data/core-usage-data-server/tsconfig.json b/packages/core/usage-data/core-usage-data-server/tsconfig.json index 77d0aa6ade3b1..83abb04d150ac 100644 --- a/packages/core/usage-data/core-usage-data-server/tsconfig.json +++ b/packages/core/usage-data/core-usage-data-server/tsconfig.json @@ -12,5 +12,8 @@ ], "exclude": [ "target/**/*", + ], + "kbn_references": [ + "@kbn/core-saved-objects-api-server", ] } diff --git a/packages/kbn-router-to-openapispec/src/__snapshots__/generate_oas.test.ts.snap b/packages/kbn-router-to-openapispec/src/__snapshots__/generate_oas.test.ts.snap index 818c0502ad774..fef935624ae64 100644 --- a/packages/kbn-router-to-openapispec/src/__snapshots__/generate_oas.test.ts.snap +++ b/packages/kbn-router-to-openapispec/src/__snapshots__/generate_oas.test.ts.snap @@ -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 { diff --git a/packages/kbn-router-to-openapispec/src/generate_oas.test.ts b/packages/kbn-router-to-openapispec/src/generate_oas.test.ts index c3532312d3088..25b786ac7c2c7 100644 --- a/packages/kbn-router-to-openapispec/src/generate_oas.test.ts +++ b/packages/kbn-router-to-openapispec/src/generate_oas.test.ts @@ -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: [ { diff --git a/packages/kbn-router-to-openapispec/src/generate_oas.test.util.ts b/packages/kbn-router-to-openapispec/src/generate_oas.test.util.ts index 898f234cdc310..a39afb6357bfc 100644 --- a/packages/kbn-router-to-openapispec/src/generate_oas.test.util.ts +++ b/packages/kbn-router-to-openapispec/src/generate_oas.test.util.ts @@ -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[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: diff --git a/packages/kbn-router-to-openapispec/src/process_router.ts b/packages/kbn-router-to-openapispec/src/process_router.ts index c66e3ee0f7bbf..af4371c3b5313 100644 --- a/packages/kbn-router-to-openapispec/src/process_router.ts +++ b/packages/kbn-router-to-openapispec/src/process_router.ts @@ -71,12 +71,13 @@ export const processRouter = ( description = `${route.options.description ?? ''}${authzDescription ?? ''}`; } + const hasDeprecations = !!route.options.deprecated; const operation: CustomOperationObject = { summary: route.options.summary ?? '', tags: route.options.tags ? extractTags(route.options.tags) : [], ...(description ? { description } : {}), ...(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 ? { diff --git a/packages/kbn-router-to-openapispec/src/process_versioned_router.test.ts b/packages/kbn-router-to-openapispec/src/process_versioned_router.test.ts index 3166b7f906985..3738c207f1f78 100644 --- a/packages/kbn-router-to-openapispec/src/process_versioned_router.test.ts +++ b/packages/kbn-router-to-openapispec/src/process_versioned_router.test.ts @@ -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(() => { @@ -167,6 +165,7 @@ describe('processVersionedRouter', () => { const createTestRoute: () => VersionedRouterRoute = () => ({ path: '/foo', method: 'get', + isVersioned: true, options: { access: 'public', deprecated: true, diff --git a/packages/kbn-router-to-openapispec/src/process_versioned_router.ts b/packages/kbn-router-to-openapispec/src/process_versioned_router.ts index c92b3a5736003..5dad5677c94ac 100644 --- a/packages/kbn-router-to-openapispec/src/process_versioned_router.ts +++ b/packages/kbn-router-to-openapispec/src/process_versioned_router.ts @@ -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 { extractAuthzDescription } from './extract_authz_description'; import type { GenerateOpenApiDocumentOptionsFilters } from './generate_oas'; @@ -102,12 +101,14 @@ 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) : [], ...(description ? { description } : {}), ...(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 ? { diff --git a/src/core/server/integration_tests/http/request_representation.test.ts b/src/core/server/integration_tests/http/request_representation.test.ts index f180a3a49ce0f..82300eceec774 100644 --- a/src/core/server/integration_tests/http/request_representation.test.ts +++ b/src/core/server/integration_tests/http/request_representation.test.ts @@ -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 }" `); }); diff --git a/src/plugins/kibana_usage_collection/server/collectors/common/counters.test.ts b/src/plugins/kibana_usage_collection/server/collectors/common/counters.test.ts index 6a64e6b597e6a..33e9b68f59c4a 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/common/counters.test.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/common/counters.test.ts @@ -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( diff --git a/src/plugins/kibana_usage_collection/server/collectors/common/counters.ts b/src/plugins/kibana_usage_collection/server/collectors/common/counters.ts index 9300ddcb959aa..df0ca67a85198 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/common/counters.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/common/counters.ts @@ -30,7 +30,7 @@ export function createCounterFetcher( filter: string, transform: (counters: CounterEvent[]) => T ) { - return async ({ soClient }: CollectorFetchContext) => { + return async ({ soClient }: Pick) => { const finder = soClient.createPointInTimeFinder({ type: USAGE_COUNTERS_SAVED_OBJECT_TYPE, namespaces: ['*'], diff --git a/src/plugins/kibana_usage_collection/server/collectors/core/fetch_deprecated_api_counters.ts b/src/plugins/kibana_usage_collection/server/collectors/core/fetch_deprecated_api_counters.ts new file mode 100644 index 0000000000000..1c90b23e40b16 --- /dev/null +++ b/src/plugins/kibana_usage_collection/server/collectors/core/fetch_deprecated_api_counters.ts @@ -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); + + return Object.values(mergedCounters); +} + +export const fetchDeprecatedApiCounterStats = (logger: Logger) => { + return createCounterFetcher(logger, DEPRECATED_API_COUNTERS_FILTER, mergeCounters); +}; diff --git a/src/plugins/kibana_usage_collection/server/collectors/core/index.ts b/src/plugins/kibana_usage_collection/server/collectors/core/index.ts index 0e0f783b0f847..e298560893ccb 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/core/index.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/core/index.ts @@ -8,3 +8,4 @@ */ export { registerCoreUsageCollector } from './core_usage_collector'; +export { fetchDeprecatedApiCounterStats } from './fetch_deprecated_api_counters'; diff --git a/src/plugins/kibana_usage_collection/server/collectors/index.ts b/src/plugins/kibana_usage_collection/server/collectors/index.ts index ef5287324ee59..c22fb3b5697f8 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/index.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/index.ts @@ -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'; diff --git a/src/plugins/kibana_usage_collection/server/plugin.ts b/src/plugins/kibana_usage_collection/server/plugin.ts index bbfc010c0e065..48fb1c6ff7b9b 100644 --- a/src/plugins/kibana_usage_collection/server/plugin.ts +++ b/src/plugins/kibana_usage_collection/server/plugin.ts @@ -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, diff --git a/test/plugin_functional/test_suites/core/deprecations.ts b/test/plugin_functional/test_suites/core/deprecations.ts index 081209e6c7dce..b41adc7ffefa5 100644 --- a/test/plugin_functional/test_suites/core/deprecations.ts +++ b/test/plugin_functional/test_suites/core/deprecations.ts @@ -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', }); }); diff --git a/x-pack/plugins/actions/server/routes/legacy/create.ts b/x-pack/plugins/actions/server/routes/legacy/create.ts index 6da0d25c2e59c..f667a9e003a77 100644 --- a/x-pack/plugins/actions/server/routes/legacy/create.ts +++ b/x-pack/plugins/actions/server/routes/legacy/create.ts @@ -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: { diff --git a/x-pack/plugins/actions/server/routes/legacy/delete.ts b/x-pack/plugins/actions/server/routes/legacy/delete.ts index 2204095e03801..c7e1e985cc6f0 100644 --- a/x-pack/plugins/actions/server/routes/legacy/delete.ts +++ b/x-pack/plugins/actions/server/routes/legacy/delete.ts @@ -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: { diff --git a/x-pack/plugins/actions/server/routes/legacy/execute.ts b/x-pack/plugins/actions/server/routes/legacy/execute.ts index 8083f884c1186..71b04262075d4 100644 --- a/x-pack/plugins/actions/server/routes/legacy/execute.ts +++ b/x-pack/plugins/actions/server/routes/legacy/execute.ts @@ -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'], }, diff --git a/x-pack/plugins/actions/server/routes/legacy/get.ts b/x-pack/plugins/actions/server/routes/legacy/get.ts index 2adf6413b9248..571849ccaf478 100644 --- a/x-pack/plugins/actions/server/routes/legacy/get.ts +++ b/x-pack/plugins/actions/server/routes/legacy/get.ts @@ -31,6 +31,7 @@ export const getActionRoute = ( options: { access: 'public', summary: `Get connector information`, + // @ts-expect-error TODO(https://github.com/elastic/kibana/issues/196095): Replace {RouteDeprecationInfo} deprecated: true, tags: ['oas-tag:connectors'], }, diff --git a/x-pack/plugins/actions/server/routes/legacy/get_all.ts b/x-pack/plugins/actions/server/routes/legacy/get_all.ts index 04ba20f4fb3c8..f0a17acb96691 100644 --- a/x-pack/plugins/actions/server/routes/legacy/get_all.ts +++ b/x-pack/plugins/actions/server/routes/legacy/get_all.ts @@ -23,6 +23,7 @@ export const getAllActionRoute = ( options: { access: 'public', summary: `Get all connectors`, + // @ts-expect-error TODO(https://github.com/elastic/kibana/issues/196095): Replace {RouteDeprecationInfo} deprecated: true, tags: ['oas-tag:connectors'], }, diff --git a/x-pack/plugins/actions/server/routes/legacy/list_action_types.ts b/x-pack/plugins/actions/server/routes/legacy/list_action_types.ts index f42cd7479286c..cc3e9c23f240d 100644 --- a/x-pack/plugins/actions/server/routes/legacy/list_action_types.ts +++ b/x-pack/plugins/actions/server/routes/legacy/list_action_types.ts @@ -27,6 +27,7 @@ export const listActionTypesRoute = ( options: { access: 'public', summary: `Get connector types`, + // @ts-expect-error TODO(https://github.com/elastic/kibana/issues/196095): Replace {RouteDeprecationInfo} deprecated: true, tags: ['oas-tag:connectors'], }, diff --git a/x-pack/plugins/actions/server/routes/legacy/update.ts b/x-pack/plugins/actions/server/routes/legacy/update.ts index 81106c2cdc73b..0bf1ec7ece55d 100644 --- a/x-pack/plugins/actions/server/routes/legacy/update.ts +++ b/x-pack/plugins/actions/server/routes/legacy/update.ts @@ -37,6 +37,7 @@ export const updateActionRoute = ( options: { access: 'public', summary: `Update a connector`, + // @ts-expect-error TODO(https://github.com/elastic/kibana/issues/196095): Replace {RouteDeprecationInfo} deprecated: true, tags: ['oas-tag:connectors'], }, diff --git a/x-pack/plugins/alerting/server/routes/legacy/create.ts b/x-pack/plugins/alerting/server/routes/legacy/create.ts index d8505fdc8453d..333877b7df49e 100644 --- a/x-pack/plugins/alerting/server/routes/legacy/create.ts +++ b/x-pack/plugins/alerting/server/routes/legacy/create.ts @@ -65,6 +65,7 @@ export const createAlertRoute = ({ access: isServerless ? 'internal' : 'public', summary: 'Create an alert', tags: ['oas-tag:alerting'], + // @ts-expect-error TODO(https://github.com/elastic/kibana/issues/196095): Replace {RouteDeprecationInfo} deprecated: true, }, }, diff --git a/x-pack/plugins/alerting/server/routes/legacy/delete.ts b/x-pack/plugins/alerting/server/routes/legacy/delete.ts index f931af10ccbbf..2b63de9e4ee73 100644 --- a/x-pack/plugins/alerting/server/routes/legacy/delete.ts +++ b/x-pack/plugins/alerting/server/routes/legacy/delete.ts @@ -33,6 +33,7 @@ export const deleteAlertRoute = ( access: isServerless ? 'internal' : 'public', summary: 'Delete an alert', tags: ['oas-tag:alerting'], + // @ts-expect-error TODO(https://github.com/elastic/kibana/issues/196095): Replace {RouteDeprecationInfo} deprecated: true, }, }, diff --git a/x-pack/plugins/alerting/server/routes/legacy/disable.ts b/x-pack/plugins/alerting/server/routes/legacy/disable.ts index 486bef89dd197..0c6f3cf062a0c 100644 --- a/x-pack/plugins/alerting/server/routes/legacy/disable.ts +++ b/x-pack/plugins/alerting/server/routes/legacy/disable.ts @@ -34,6 +34,7 @@ export const disableAlertRoute = ( access: isServerless ? 'internal' : 'public', summary: 'Disable an alert', tags: ['oas-tag:alerting'], + // @ts-expect-error TODO(https://github.com/elastic/kibana/issues/196095): Replace {RouteDeprecationInfo} deprecated: true, }, }, diff --git a/x-pack/plugins/alerting/server/routes/legacy/enable.ts b/x-pack/plugins/alerting/server/routes/legacy/enable.ts index c5076b3de1a54..d52eaa784f670 100644 --- a/x-pack/plugins/alerting/server/routes/legacy/enable.ts +++ b/x-pack/plugins/alerting/server/routes/legacy/enable.ts @@ -35,6 +35,7 @@ export const enableAlertRoute = ( access: isServerless ? 'internal' : 'public', summary: 'Enable an alert', tags: ['oas-tag:alerting'], + // @ts-expect-error TODO(https://github.com/elastic/kibana/issues/196095): Replace {RouteDeprecationInfo} deprecated: true, }, }, diff --git a/x-pack/plugins/alerting/server/routes/legacy/find.ts b/x-pack/plugins/alerting/server/routes/legacy/find.ts index ad85f3c7333b0..fa309ae51f2e4 100644 --- a/x-pack/plugins/alerting/server/routes/legacy/find.ts +++ b/x-pack/plugins/alerting/server/routes/legacy/find.ts @@ -79,6 +79,7 @@ export const findAlertRoute = ( tags: ['oas-tag:alerting'], description: 'Gets a paginated set of alerts. Alert `params` are stored as a flattened field type and analyzed as keywords. As alerts change in Kibana, the results on each page of the response also change. Use the find API for traditional paginated results, but avoid using it to export large amounts of data.', + // @ts-expect-error TODO(https://github.com/elastic/kibana/issues/196095): Replace {RouteDeprecationInfo} deprecated: true, }, }, diff --git a/x-pack/plugins/alerting/server/routes/legacy/get.ts b/x-pack/plugins/alerting/server/routes/legacy/get.ts index 8437b888f7c0f..e5eff52bf02d6 100644 --- a/x-pack/plugins/alerting/server/routes/legacy/get.ts +++ b/x-pack/plugins/alerting/server/routes/legacy/get.ts @@ -33,6 +33,7 @@ export const getAlertRoute = ( access: isServerless ? 'internal' : 'public', summary: 'Get an alert', tags: ['oas-tag:alerting'], + // @ts-expect-error TODO(https://github.com/elastic/kibana/issues/196095): Replace {RouteDeprecationInfo} deprecated: true, }, }, diff --git a/x-pack/plugins/alerting/server/routes/legacy/get_alert_instance_summary.ts b/x-pack/plugins/alerting/server/routes/legacy/get_alert_instance_summary.ts index fd1fae64b538f..58a75dd68dce7 100644 --- a/x-pack/plugins/alerting/server/routes/legacy/get_alert_instance_summary.ts +++ b/x-pack/plugins/alerting/server/routes/legacy/get_alert_instance_summary.ts @@ -44,6 +44,7 @@ export const getAlertInstanceSummaryRoute = ( access: isServerless ? 'internal' : 'public', summary: 'Get an alert summary', tags: ['oas-tag:alerting'], + // @ts-expect-error TODO(https://github.com/elastic/kibana/issues/196095): Replace {RouteDeprecationInfo} deprecated: true, }, }, diff --git a/x-pack/plugins/alerting/server/routes/legacy/get_alert_state.ts b/x-pack/plugins/alerting/server/routes/legacy/get_alert_state.ts index 5943ab7203599..e952ef8719667 100644 --- a/x-pack/plugins/alerting/server/routes/legacy/get_alert_state.ts +++ b/x-pack/plugins/alerting/server/routes/legacy/get_alert_state.ts @@ -33,6 +33,7 @@ export const getAlertStateRoute = ( access: isServerless ? 'internal' : 'public', summary: 'Get the state of an alert', tags: ['oas-tag:alerting'], + // @ts-expect-error TODO(https://github.com/elastic/kibana/issues/196095): Replace {RouteDeprecationInfo} deprecated: true, }, }, diff --git a/x-pack/plugins/alerting/server/routes/legacy/health.ts b/x-pack/plugins/alerting/server/routes/legacy/health.ts index 90bfda371932a..8f67767941fd2 100644 --- a/x-pack/plugins/alerting/server/routes/legacy/health.ts +++ b/x-pack/plugins/alerting/server/routes/legacy/health.ts @@ -29,6 +29,7 @@ export function healthRoute( access: isServerless ? 'internal' : 'public', summary: 'Get the alerting framework health', tags: ['oas-tag:alerting'], + // @ts-expect-error TODO(https://github.com/elastic/kibana/issues/196095): Replace {RouteDeprecationInfo} deprecated: true, }, }, diff --git a/x-pack/plugins/alerting/server/routes/legacy/list_alert_types.ts b/x-pack/plugins/alerting/server/routes/legacy/list_alert_types.ts index 6668ff219ade0..35d6a7efeeee3 100644 --- a/x-pack/plugins/alerting/server/routes/legacy/list_alert_types.ts +++ b/x-pack/plugins/alerting/server/routes/legacy/list_alert_types.ts @@ -25,6 +25,7 @@ export const listAlertTypesRoute = ( access: isServerless ? 'internal' : 'public', summary: 'Get the alert types', tags: ['oas-tag:alerting'], + // @ts-expect-error TODO(https://github.com/elastic/kibana/issues/196095): Replace {RouteDeprecationInfo} deprecated: true, }, }, diff --git a/x-pack/plugins/alerting/server/routes/legacy/mute_all.ts b/x-pack/plugins/alerting/server/routes/legacy/mute_all.ts index eaa989dc8fb6a..5c4fc1542ef5b 100644 --- a/x-pack/plugins/alerting/server/routes/legacy/mute_all.ts +++ b/x-pack/plugins/alerting/server/routes/legacy/mute_all.ts @@ -34,6 +34,7 @@ export const muteAllAlertRoute = ( access: isServerless ? 'internal' : 'public', summary: 'Mute all alert instances', tags: ['oas-tag:alerting'], + // @ts-expect-error TODO(https://github.com/elastic/kibana/issues/196095): Replace {RouteDeprecationInfo} deprecated: true, }, }, diff --git a/x-pack/plugins/alerting/server/routes/legacy/mute_instance.ts b/x-pack/plugins/alerting/server/routes/legacy/mute_instance.ts index c309dd36b7744..ab0b52d41de29 100644 --- a/x-pack/plugins/alerting/server/routes/legacy/mute_instance.ts +++ b/x-pack/plugins/alerting/server/routes/legacy/mute_instance.ts @@ -37,6 +37,7 @@ export const muteAlertInstanceRoute = ( access: isServerless ? 'internal' : 'public', summary: 'Mute an alert', tags: ['oas-tag:alerting'], + // @ts-expect-error TODO(https://github.com/elastic/kibana/issues/196095): Replace {RouteDeprecationInfo} deprecated: true, }, }, diff --git a/x-pack/plugins/alerting/server/routes/legacy/unmute_all.ts b/x-pack/plugins/alerting/server/routes/legacy/unmute_all.ts index 70dfd65e33c79..0681e7d2cf01e 100644 --- a/x-pack/plugins/alerting/server/routes/legacy/unmute_all.ts +++ b/x-pack/plugins/alerting/server/routes/legacy/unmute_all.ts @@ -34,6 +34,7 @@ export const unmuteAllAlertRoute = ( access: isServerless ? 'internal' : 'public', summary: 'Unmute all alert instances', tags: ['oas-tag:alerting'], + // @ts-expect-error TODO(https://github.com/elastic/kibana/issues/196095): Replace {RouteDeprecationInfo} deprecated: true, }, }, diff --git a/x-pack/plugins/alerting/server/routes/legacy/unmute_instance.ts b/x-pack/plugins/alerting/server/routes/legacy/unmute_instance.ts index 7990539d6c20d..1101a2b5092e7 100644 --- a/x-pack/plugins/alerting/server/routes/legacy/unmute_instance.ts +++ b/x-pack/plugins/alerting/server/routes/legacy/unmute_instance.ts @@ -35,6 +35,7 @@ export const unmuteAlertInstanceRoute = ( access: isServerless ? 'internal' : 'public', summary: 'Unmute an alert', tags: ['oas-tag:alerting'], + // @ts-expect-error TODO(https://github.com/elastic/kibana/issues/196095): Replace {RouteDeprecationInfo} deprecated: true, }, }, diff --git a/x-pack/plugins/alerting/server/routes/legacy/update.ts b/x-pack/plugins/alerting/server/routes/legacy/update.ts index b65579e17b087..01adeb5c634dc 100644 --- a/x-pack/plugins/alerting/server/routes/legacy/update.ts +++ b/x-pack/plugins/alerting/server/routes/legacy/update.ts @@ -61,6 +61,7 @@ export const updateAlertRoute = ( access: isServerless ? 'internal' : 'public', summary: 'Update an alert', tags: ['oas-tag:alerting'], + // @ts-expect-error TODO(https://github.com/elastic/kibana/issues/196095): Replace {RouteDeprecationInfo} deprecated: true, }, }, diff --git a/x-pack/plugins/alerting/server/routes/legacy/update_api_key.ts b/x-pack/plugins/alerting/server/routes/legacy/update_api_key.ts index 06c466333967c..30c51d3cdcf5c 100644 --- a/x-pack/plugins/alerting/server/routes/legacy/update_api_key.ts +++ b/x-pack/plugins/alerting/server/routes/legacy/update_api_key.ts @@ -35,6 +35,7 @@ export const updateApiKeyRoute = ( access: isServerless ? 'internal' : 'public', summary: 'Update the API key for an alert', tags: ['oas-tag:alerting'], + // @ts-expect-error TODO(https://github.com/elastic/kibana/issues/196095): Replace {RouteDeprecationInfo} deprecated: true, }, }, diff --git a/x-pack/plugins/cases/server/routes/api/comments/get_all_comment.ts b/x-pack/plugins/cases/server/routes/api/comments/get_all_comment.ts index a33b638f5310e..6e8ac79bffec9 100644 --- a/x-pack/plugins/cases/server/routes/api/comments/get_all_comment.ts +++ b/x-pack/plugins/cases/server/routes/api/comments/get_all_comment.ts @@ -31,6 +31,7 @@ export const getAllCommentsRoute = createCasesRoute({ summary: `Gets all case comments`, tags: ['oas-tag:cases'], // description: 'You must have `read` privileges for the **Cases** feature in the **Management**, **Observability**, or **Security** section of the Kibana feature privileges, depending on the owner of the cases with the comments you\'re seeking.', + // @ts-expect-error TODO(https://github.com/elastic/kibana/issues/196095): Replace {RouteDeprecationInfo} deprecated: true, }, handler: async ({ context, request, response }) => { diff --git a/x-pack/plugins/cases/server/routes/api/stats/get_status.ts b/x-pack/plugins/cases/server/routes/api/stats/get_status.ts index 07d02c0b6713f..dce369e4a0f45 100644 --- a/x-pack/plugins/cases/server/routes/api/stats/get_status.ts +++ b/x-pack/plugins/cases/server/routes/api/stats/get_status.ts @@ -26,6 +26,7 @@ export const getStatusRoute: CaseRoute = createCasesRoute({ description: 'Returns the number of cases that are open, closed, and in progress in the default space.', // You must have `read` privileges for the **Cases** feature in the **Management**, **Observability**, or **Security** section of the Kibana feature privileges, depending on the owner of the cases you're seeking. + // @ts-expect-error TODO(https://github.com/elastic/kibana/issues/196095): Replace {RouteDeprecationInfo} deprecated: true, }, handler: async ({ context, request, response }) => { diff --git a/x-pack/plugins/cases/server/routes/api/user_actions/get_all_user_actions.ts b/x-pack/plugins/cases/server/routes/api/user_actions/get_all_user_actions.ts index 5edc7a261b3c4..17fe0dcdb9012 100644 --- a/x-pack/plugins/cases/server/routes/api/user_actions/get_all_user_actions.ts +++ b/x-pack/plugins/cases/server/routes/api/user_actions/get_all_user_actions.ts @@ -30,6 +30,7 @@ export const getUserActionsRoute = createCasesRoute({ description: `Returns all user activity for a case.`, // You must have `read` privileges for the **Cases** feature in the **Management**, **Observability**, or **Security** section of the Kibana feature privileges, depending on the owner of the case you're seeking. tags: ['oas-tag:cases'], + // @ts-expect-error TODO(https://github.com/elastic/kibana/issues/196095): Replace {RouteDeprecationInfo} deprecated: true, }, handler: async ({ context, request, response }) => { diff --git a/x-pack/plugins/ml/server/routes/system.ts b/x-pack/plugins/ml/server/routes/system.ts index 0804a8dc02348..bf4fa3161f5b9 100644 --- a/x-pack/plugins/ml/server/routes/system.ts +++ b/x-pack/plugins/ml/server/routes/system.ts @@ -202,8 +202,9 @@ export function systemRoutes( options: { tags: ['access:ml:canGetJobs'], }, - deprecated: true, summary: 'ES Search wrapper', + // @ts-expect-error TODO(https://github.com/elastic/kibana/issues/196095): Replace {RouteDeprecationInfo} + deprecated: true, }) .addVersion( { diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/index.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/index.ts index 9603e9e9a6d48..2f6e46d1d7727 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/index.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/index.ts @@ -95,6 +95,7 @@ export function registerEndpointRoutes( access: 'public', path: METADATA_TRANSFORMS_STATUS_ROUTE, options: { authRequired: true, tags: ['access:securitySolution'] }, + // @ts-expect-error TODO(https://github.com/elastic/kibana/issues/196095): Replace {RouteDeprecationInfo} deprecated: true, }) .addVersion( diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/suggestions/index.ts b/x-pack/plugins/security_solution/server/endpoint/routes/suggestions/index.ts index a4853d9772ad7..677fb004ee862 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/suggestions/index.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/suggestions/index.ts @@ -43,6 +43,7 @@ export function registerEndpointSuggestionsRoutes( access: 'public', path: SUGGESTIONS_ROUTE, options: { authRequired: true, tags: ['access:securitySolution'] }, + // @ts-expect-error TODO(https://github.com/elastic/kibana/issues/196095): Replace {RouteDeprecationInfo} deprecated: true, }) .addVersion( diff --git a/x-pack/plugins/upgrade_assistant/README.md b/x-pack/plugins/upgrade_assistant/README.md index 30b403f6d6230..2acac8e3e734d 100644 --- a/x-pack/plugins/upgrade_assistant/README.md +++ b/x-pack/plugins/upgrade_assistant/README.md @@ -17,7 +17,7 @@ When we want to enable ML model snapshot deprecation warnings again we need to c * Migrating system indices (`featureSet.migrateSystemIndices`): Migrating system indices should only be enabled for major version upgrades. This config hides the second step from the UA UI for migrating system indices. * Reindex corrective actions (`featureSet.reindexCorrectiveActions`): Deprecations with reindexing corrective actions are only enabled for major version upgrades. Currently, the reindex actions include some logic that is specific to the [8.0 upgrade](https://github.com/elastic/kibana/blob/main/x-pack/plugins/upgrade_assistant/server/lib/reindexing/index_settings.ts). End users could get into a bad situation if this is enabled before this logic is fixed. -### Deprecations +## Deprecations There are three sources of deprecation information: @@ -275,6 +275,47 @@ PUT .reporting-*/_settings } ``` +#### Kibana API deprecations: +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"}} + ] + } + } +} + +``` + For a complete list of Kibana deprecations, refer to the [8.0 Kibana deprecations meta issue](https://github.com/elastic/kibana/issues/109166). ### Errors diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/deprecation_details_flyout.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/deprecation_details_flyout.tsx index a6ea9a26c9bb8..beb4c7c0c678c 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/deprecation_details_flyout.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/deprecation_details_flyout.tsx @@ -50,6 +50,12 @@ const i18nTexts = { defaultMessage: 'Quick resolve', } ), + markAsResolvedButtonLabel: i18n.translate( + 'xpack.upgradeAssistant.kibanaDeprecations.flyout.quickResolveButtonLabel', + { + defaultMessage: 'Mark as Resolved', + } + ), retryQuickResolveButtonLabel: i18n.translate( 'xpack.upgradeAssistant.kibanaDeprecations.flyout.retryQuickResolveButtonLabel', { @@ -97,7 +103,15 @@ const i18nTexts = { ), }; -const getQuickResolveButtonLabel = (deprecationResolutionState?: DeprecationResolutionState) => { +interface AvailableCorrectiveActions { + api: boolean; + manual: boolean; + markAsResolved: boolean; +} +const getQuickResolveButtonLabel = ( + deprecationResolutionState: DeprecationResolutionState | undefined, + avilableCorrectiveActions: AvailableCorrectiveActions +) => { if (deprecationResolutionState?.resolveDeprecationStatus === 'in_progress') { return i18nTexts.quickResolveInProgressButtonLabel; } @@ -110,7 +124,13 @@ const getQuickResolveButtonLabel = (deprecationResolutionState?: DeprecationReso return i18nTexts.retryQuickResolveButtonLabel; } - return i18nTexts.quickResolveButtonLabel; + if (avilableCorrectiveActions.api) { + return i18nTexts.quickResolveButtonLabel; + } + + if (avilableCorrectiveActions.markAsResolved) { + return i18nTexts.markAsResolvedButtonLabel; + } }; export const DeprecationDetailsFlyout = ({ @@ -120,9 +140,19 @@ export const DeprecationDetailsFlyout = ({ deprecationResolutionState, }: DeprecationDetailsFlyoutProps) => { const { documentationUrl, message, correctiveActions, title } = deprecation; + const messages = Array.isArray(message) ? message : [message]; + const isCurrent = deprecationResolutionState?.id === deprecation.id; + const avilableCorrectiveActions: AvailableCorrectiveActions = { + api: !!correctiveActions.api, + manual: correctiveActions.manualSteps && correctiveActions.manualSteps.length > 0, + markAsResolved: !!correctiveActions.mark_as_resolved_api, + }; const isResolved = isCurrent && deprecationResolutionState?.resolveDeprecationStatus === 'ok'; + const hasResolveButton = + avilableCorrectiveActions.api || avilableCorrectiveActions.markAsResolved; + const onResolveDeprecation = useCallback(() => { uiMetricService.trackUiMetric(METRIC_TYPE.CLICK, UIM_KIBANA_QUICK_RESOLVE_CLICK); resolveDeprecation(deprecation); @@ -155,7 +185,11 @@ export const DeprecationDetailsFlyout = ({ )} -

{message}

+ {messages.map((m, i) => ( +

+ {m} +

+ ))} {documentationUrl && (

@@ -221,7 +255,7 @@ export const DeprecationDetailsFlyout = ({ {/* Only show the "Quick resolve" button if deprecation supports it and deprecation is not yet resolved */} - {correctiveActions.api && !isResolved && ( + {hasResolveButton && !isResolved && ( - {getQuickResolveButtonLabel(deprecationResolutionState)} + {getQuickResolveButtonLabel(deprecationResolutionState, avilableCorrectiveActions)} )} diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/kibana_deprecations.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/kibana_deprecations.tsx index d76f1afa9e612..0d433a59ee2d9 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/kibana_deprecations.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/kibana_deprecations.tsx @@ -126,9 +126,9 @@ export const KibanaDeprecationsList = ({ const [flyoutContent, setFlyoutContent] = useState( undefined ); - const [deprecationResolutionState, setDeprecationResolutionState] = useState< - DeprecationResolutionState | undefined - >(undefined); + const [deprecationResolutionStates, setDeprecationResolutionStates] = useState< + Record + >({}); const { services: { @@ -194,17 +194,25 @@ export const KibanaDeprecationsList = ({ const resolveDeprecation = useCallback( async (deprecationDetails: KibanaDeprecationDetails) => { - setDeprecationResolutionState({ - id: deprecationDetails.id, - resolveDeprecationStatus: 'in_progress', + setDeprecationResolutionStates((states) => { + states[deprecationDetails.id] = { + id: deprecationDetails.id, + resolveDeprecationStatus: 'in_progress', + }; + + return states; }); const response = await deprecations.resolveDeprecation(deprecationDetails); - setDeprecationResolutionState({ - id: deprecationDetails.id, - resolveDeprecationStatus: response.status, - resolveDeprecationError: response.status === 'fail' ? response.reason : undefined, + setDeprecationResolutionStates((states) => { + states[deprecationDetails.id] = { + id: deprecationDetails.id, + resolveDeprecationStatus: response.status, + resolveDeprecationError: response.status === 'fail' ? response.reason : undefined, + }; + + return states; }); closeFlyout(); @@ -221,10 +229,7 @@ export const KibanaDeprecationsList = ({ deprecation: flyoutContent, closeFlyout, resolveDeprecation, - deprecationResolutionState: - deprecationResolutionState && flyoutContent.id === deprecationResolutionState.id - ? deprecationResolutionState - : undefined, + deprecationResolutionState: deprecationResolutionStates[flyoutContent.id], }, flyoutProps: { onClose: closeFlyout, @@ -236,7 +241,7 @@ export const KibanaDeprecationsList = ({ }, [ addContentToGlobalFlyout, closeFlyout, - deprecationResolutionState, + deprecationResolutionStates, flyoutContent, resolveDeprecation, ]); @@ -310,7 +315,7 @@ export const KibanaDeprecationsList = ({ deprecations={kibanaDeprecations} reload={getAllDeprecations} toggleFlyout={toggleFlyout} - deprecationResolutionState={deprecationResolutionState} + deprecationResolutionStates={deprecationResolutionStates} /> ); diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/kibana_deprecations_table.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/kibana_deprecations_table.tsx index 6a757d0cb2b0b..8d223dedca490 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/kibana_deprecations_table.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/kibana_deprecations_table.tsx @@ -56,6 +56,12 @@ const i18nTexts = { defaultMessage: 'Feature', } ), + apiDeprecationTypeCellLabel: i18n.translate( + 'xpack.upgradeAssistant.kibanaDeprecations.table.apiDeprecationTypeCellLabel', + { + defaultMessage: 'API', + } + ), unknownDeprecationTypeCellLabel: i18n.translate( 'xpack.upgradeAssistant.kibanaDeprecations.table.unknownDeprecationTypeCellLabel', { @@ -86,14 +92,14 @@ interface Props { deprecations?: KibanaDeprecationDetails[]; reload: () => void; toggleFlyout: (newFlyoutContent?: KibanaDeprecationDetails) => void; - deprecationResolutionState?: DeprecationResolutionState; + deprecationResolutionStates: Record; } export const KibanaDeprecationsTable: React.FunctionComponent = ({ deprecations, reload, toggleFlyout, - deprecationResolutionState, + deprecationResolutionStates, }) => { const columns: Array> = [ { @@ -135,6 +141,8 @@ export const KibanaDeprecationsTable: React.FunctionComponent = ({ return i18nTexts.configDeprecationTypeCellLabel; case 'feature': return i18nTexts.featureDeprecationTypeCellLabel; + case 'api': + return i18nTexts.apiDeprecationTypeCellLabel; case 'uncategorized': default: return i18nTexts.unknownDeprecationTypeCellLabel; @@ -155,7 +163,8 @@ export const KibanaDeprecationsTable: React.FunctionComponent = ({ ); }, @@ -191,6 +200,10 @@ export const KibanaDeprecationsTable: React.FunctionComponent = ({ value: 'feature', name: i18nTexts.featureDeprecationTypeCellLabel, }, + { + value: 'api', + name: i18nTexts.apiDeprecationTypeCellLabel, + }, { value: 'uncategorized', name: i18nTexts.unknownDeprecationTypeCellLabel, diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/resolution_table_cell.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/resolution_table_cell.tsx index 373c9e7b43f52..502c31ae90744 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/resolution_table_cell.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/resolution_table_cell.tsx @@ -19,7 +19,7 @@ import { i18n } from '@kbn/i18n'; import type { DeprecationResolutionState } from './kibana_deprecations'; -const i18nTexts = { +const manualI18nTexts = { manualCellLabel: i18n.translate( 'xpack.upgradeAssistant.kibanaDeprecations.table.manualCellLabel', { @@ -32,31 +32,34 @@ const i18nTexts = { defaultMessage: 'This issue needs to be resolved manually.', } ), - automatedCellLabel: i18n.translate( +}; + +const automatedI18nTexts = { + resolutionTypeCellLabel: i18n.translate( 'xpack.upgradeAssistant.kibanaDeprecations.table.automatedCellLabel', { defaultMessage: 'Automated', } ), - automationInProgressCellLabel: i18n.translate( + resolutionProgressCellLabel: i18n.translate( 'xpack.upgradeAssistant.kibanaDeprecations.table.automationInProgressCellLabel', { defaultMessage: 'Resolution in progress…', } ), - automationCompleteCellLabel: i18n.translate( + resolutionCompleteCellLabel: i18n.translate( 'xpack.upgradeAssistant.kibanaDeprecations.table.automationCompleteCellLabel', { defaultMessage: 'Resolved', } ), - automationFailedCellLabel: i18n.translate( + resolutionFailedCellLabel: i18n.translate( 'xpack.upgradeAssistant.kibanaDeprecations.table.automationFailedCellLabel', { defaultMessage: 'Resolution failed', } ), - automatedCellTooltipLabel: i18n.translate( + resolutionCellTooltipLabel: i18n.translate( 'xpack.upgradeAssistant.kibanaDeprecations.table.automatedCellTooltipLabel', { defaultMessage: 'This issue can be resolved automatically.', @@ -64,18 +67,56 @@ const i18nTexts = { ), }; +const markAsResolvedI18nTexts = { + resolutionTypeCellLabel: i18n.translate( + 'xpack.upgradeAssistant.kibanaDeprecations.table.markAsResolvedCellLabel', + { + defaultMessage: 'Mark as resolved', + } + ), + resolutionProgressCellLabel: i18n.translate( + 'xpack.upgradeAssistant.kibanaDeprecations.table.markAsResolvedInProgressCellLabel', + { + defaultMessage: 'Marking as resolved…', + } + ), + resolutionCompleteCellLabel: i18n.translate( + 'xpack.upgradeAssistant.kibanaDeprecations.table.markAsResolvedCompleteCellLabel', + { + defaultMessage: 'Marked as resolved', + } + ), + resolutionFailedCellLabel: i18n.translate( + 'xpack.upgradeAssistant.kibanaDeprecations.table.markAsResolvedFailedCellLabel', + { + defaultMessage: 'Failed to mark as resolved', + } + ), + resolutionCellTooltipLabel: i18n.translate( + 'xpack.upgradeAssistant.kibanaDeprecations.table.markAsResolvedCellTooltipLabel', + { + defaultMessage: 'This issue can be marked as resolved.', + } + ), +}; + interface Props { deprecationId: string; isAutomated: boolean; + canBeMarkedAsResolved: boolean; deprecationResolutionState?: DeprecationResolutionState; } export const ResolutionTableCell: React.FunctionComponent = ({ deprecationId, isAutomated, + canBeMarkedAsResolved, deprecationResolutionState, }) => { - if (isAutomated) { + if (isAutomated || canBeMarkedAsResolved) { + const resolutionI18nTexts = isAutomated ? automatedI18nTexts : markAsResolvedI18nTexts; + const euiIconType = isAutomated ? 'indexSettings' : 'clickLeft'; + if (deprecationResolutionState?.id === deprecationId) { const { resolveDeprecationStatus } = deprecationResolutionState; @@ -87,7 +128,7 @@ export const ResolutionTableCell: React.FunctionComponent = ({ - {i18nTexts.automationInProgressCellLabel} + {resolutionI18nTexts.resolutionProgressCellLabel} ); @@ -98,7 +139,7 @@ export const ResolutionTableCell: React.FunctionComponent = ({ - {i18nTexts.automationFailedCellLabel} + {resolutionI18nTexts.resolutionFailedCellLabel} ); @@ -110,7 +151,7 @@ export const ResolutionTableCell: React.FunctionComponent = ({ - {i18nTexts.automationCompleteCellLabel} + {resolutionI18nTexts.resolutionCompleteCellLabel} ); @@ -118,13 +159,13 @@ export const ResolutionTableCell: React.FunctionComponent = ({ } return ( - + - + - {i18nTexts.automatedCellLabel} + {resolutionI18nTexts.resolutionTypeCellLabel} @@ -134,11 +175,11 @@ export const ResolutionTableCell: React.FunctionComponent = ({ return ( - {i18nTexts.manualCellLabel} + {manualI18nTexts.manualCellLabel} ); diff --git a/x-pack/test/tsconfig.json b/x-pack/test/tsconfig.json index 03fbddf161f00..2ba14ceb1218c 100644 --- a/x-pack/test/tsconfig.json +++ b/x-pack/test/tsconfig.json @@ -185,6 +185,8 @@ "@kbn/cloud-security-posture-common", "@kbn/saved-objects-management-plugin", "@kbn/alerting-types", - "@kbn/ai-assistant-common" + "@kbn/ai-assistant-common", + "@kbn/core-deprecations-common", + "@kbn/usage-collection-plugin" ] } diff --git a/x-pack/test/upgrade_assistant_integration/config.js b/x-pack/test/upgrade_assistant_integration/config.ts similarity index 70% rename from x-pack/test/upgrade_assistant_integration/config.js rename to x-pack/test/upgrade_assistant_integration/config.ts index 9529e4bc568d3..0794f4d0b9ada 100644 --- a/x-pack/test/upgrade_assistant_integration/config.js +++ b/x-pack/test/upgrade_assistant_integration/config.ts @@ -6,8 +6,10 @@ */ import { commonFunctionalServices } from '@kbn/ftr-common-functional-services'; +import { FtrConfigProviderContext } from '@kbn/test'; +import path from 'node:path'; -export default async function ({ readConfigFile }) { +export default async function ({ readConfigFile }: FtrConfigProviderContext) { // Read the Kibana API integration tests config file so that we can utilize its services. const kibanaAPITestsConfig = await readConfigFile( require.resolve('@kbn/test-suites-src/api_integration/config') @@ -26,7 +28,14 @@ export default async function ({ readConfigFile }) { junit: { reportName: 'X-Pack Upgrade Assistant Integration Tests', }, - kbnTestServer: xPackFunctionalTestsConfig.get('kbnTestServer'), + kbnTestServer: { + ...xPackFunctionalTestsConfig.get('kbnTestServer'), + serverArgs: [ + ...xPackFunctionalTestsConfig.get('kbnTestServer.serverArgs'), + `--plugin-path=${path.resolve(__dirname, '../../../examples/routing_example')}`, + `--plugin-path=${path.resolve(__dirname, '../../../examples/developer_examples')}`, + ], + }, esTestCluster: { ...xPackFunctionalTestsConfig.get('esTestCluster'), // this archive can not be loaded into 8.0+ diff --git a/x-pack/test/upgrade_assistant_integration/upgrade_assistant/api_deprecations.ts b/x-pack/test/upgrade_assistant_integration/upgrade_assistant/api_deprecations.ts new file mode 100644 index 0000000000000..f146bf38f5f26 --- /dev/null +++ b/x-pack/test/upgrade_assistant_integration/upgrade_assistant/api_deprecations.ts @@ -0,0 +1,162 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { expect as expectExpect } from 'expect'; +import type { DomainDeprecationDetails } from '@kbn/core-deprecations-common'; +import { ApiDeprecationDetails } from '@kbn/core-deprecations-common/src/types'; +import { setTimeout as setTimeoutAsync } from 'timers/promises'; +import { UsageCountersSavedObject } from '@kbn/usage-collection-plugin/server'; +import _ from 'lodash'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; + +interface DomainApiDeprecationDetails extends ApiDeprecationDetails { + domainId: string; +} + +const getApiDeprecations = (allDeprecations: DomainDeprecationDetails[]) => { + return allDeprecations.filter( + (deprecation) => deprecation.deprecationType === 'api' + ) as unknown as DomainApiDeprecationDetails[]; +}; + +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + const retry = getService('retry'); + const es = getService('es'); + + describe('Kibana API Deprecations', () => { + before(async () => { + // await kibanaServer.savedObjects.cleanStandardList(); + await esArchiver.emptyKibanaIndex(); + }); + it('returns does not return api deprecations if the routes are not called', async () => { + const { deprecations } = (await supertest.get(`/api/deprecations/`).expect(200)).body; + const apiDeprecations = getApiDeprecations(deprecations); + expect(apiDeprecations.length).to.equal(0); + }); + + it('returns deprecated APIs when the api is called', async () => { + await supertest.get(`/api/routing_example/d/removed_route`).expect(200); + + // sleep a little until the usage counter is synced into ES + await setTimeoutAsync(3000); + await retry.tryForTime( + 15 * 1000, + async () => { + const { deprecations } = (await supertest.get(`/api/deprecations/`).expect(200)).body; + const apiDeprecations = getApiDeprecations(deprecations); + expect(apiDeprecations.length).to.equal(1); + + expectExpect(apiDeprecations[0].correctiveActions.mark_as_resolved_api).toEqual({ + routePath: '/api/routing_example/d/removed_route', + routeMethod: 'get', + apiTotalCalls: 1, + totalMarkedAsResolved: 0, + timestamp: expectExpect.any(String), + }); + + expectExpect(apiDeprecations[0].domainId).toEqual('core.api_deprecations'); + expectExpect(apiDeprecations[0].apiId).toEqual( + 'unversioned|get|/api/routing_example/d/removed_route' + ); + expectExpect(apiDeprecations[0].title).toEqual( + 'The "GET /api/routing_example/d/removed_route" route is removed' + ); + }, + undefined, + 2000 + ); + }); + + it('no longer returns deprecated API when it is marked as resolved', async () => { + await supertest + .post(`/api/deprecations/mark_as_resolved`) + .set('kbn-xsrf', 'xxx') + .send({ + domainId: 'core.api_deprecations', + routePath: '/api/routing_example/d/removed_route', + routeMethod: 'get', + incrementBy: 1, + }) + .expect(200); + + // sleep a little until the usage counter is synced into ES + await setTimeoutAsync(5000); + await retry.tryForTime(15 * 1000, async () => { + const { deprecations } = (await supertest.get(`/api/deprecations/`).expect(200)).body; + const apiDeprecations = getApiDeprecations(deprecations); + expect(apiDeprecations.length).to.equal(0); + }); + }); + + it('returns deprecated API when it is called again after resolved, but with a different message', async () => { + await supertest.get(`/api/routing_example/d/removed_route`).expect(200); + + // sleep a little until the usage counter is synced into ES + await setTimeoutAsync(3000); + await retry.tryForTime( + 15 * 1000, + async () => { + const { deprecations } = (await supertest.get(`/api/deprecations/`).expect(200)).body; + const apiDeprecations = getApiDeprecations(deprecations); + expect(apiDeprecations.length).to.equal(1); + + expectExpect(apiDeprecations[0].correctiveActions.mark_as_resolved_api).toEqual({ + routePath: '/api/routing_example/d/removed_route', + routeMethod: 'get', + apiTotalCalls: 2, + totalMarkedAsResolved: 1, + timestamp: expectExpect.any(String), + }); + }, + undefined, + 2000 + ); + }); + + it('keeps track of all counters via saved objects and core usage counters', async () => { + const should = ['total', 'resolved', 'marked_as_resolved'].map((type) => ({ + match: { 'usage-counter.counterType': `deprecated_api_call:${type}` }, + })); + + const { hits } = await es.search<{ 'usage-counter': UsageCountersSavedObject }>({ + index: '.kibana_usage_counters', + body: { + query: { bool: { should } }, + }, + }); + + expect(hits.hits.length).to.equal(3); + const counters = hits.hits.map((hit) => hit._source!['usage-counter']).sort(); + expectExpect(_.sortBy(counters, 'counterType')).toEqual([ + { + count: 1, + counterName: 'unversioned|get|/api/routing_example/d/removed_route', + counterType: 'deprecated_api_call:marked_as_resolved', + domainId: 'core', + source: 'server', + }, + { + count: 1, + counterName: 'unversioned|get|/api/routing_example/d/removed_route', + counterType: 'deprecated_api_call:resolved', + domainId: 'core', + source: 'server', + }, + { + count: 2, + counterName: 'unversioned|get|/api/routing_example/d/removed_route', + counterType: 'deprecated_api_call:total', + domainId: 'core', + source: 'server', + }, + ]); + }); + }); +} diff --git a/x-pack/test/upgrade_assistant_integration/upgrade_assistant/index.js b/x-pack/test/upgrade_assistant_integration/upgrade_assistant/index.ts similarity index 64% rename from x-pack/test/upgrade_assistant_integration/upgrade_assistant/index.js rename to x-pack/test/upgrade_assistant_integration/upgrade_assistant/index.ts index eb09d24b79b6a..2aaddc7d6f669 100644 --- a/x-pack/test/upgrade_assistant_integration/upgrade_assistant/index.js +++ b/x-pack/test/upgrade_assistant_integration/upgrade_assistant/index.ts @@ -5,8 +5,11 @@ * 2.0. */ -export default function ({ loadTestFile }) { +import { FtrProviderContext } from '../../common/ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { describe('upgrade assistant', function () { loadTestFile(require.resolve('./reindexing')); + loadTestFile(require.resolve('./api_deprecations')); }); }