diff --git a/packages/core/deprecations/core-deprecations-common/src/types.ts b/packages/core/deprecations/core-deprecations-common/src/types.ts index bf9a4a673d721..6effe7323e25f 100644 --- a/packages/core/deprecations/core-deprecations-common/src/types.ts +++ b/packages/core/deprecations/core-deprecations-common/src/types.ts @@ -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' | 'feature' | 'api'; /** (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. */ @@ -91,7 +91,19 @@ export interface FeatureDeprecationDetails extends BaseDeprecationDetails { /** * @public */ -export type DeprecationsDetails = ConfigDeprecationDetails | FeatureDeprecationDetails; +export interface ApiDeprecationDetails extends BaseDeprecationDetails { + routePath: string; + routeMethod: string; + deprecationType: 'api'; +} + +/** + * @public + */ +export type DeprecationsDetails = + | ConfigDeprecationDetails + | FeatureDeprecationDetails + | ApiDeprecationDetails; /** * @public diff --git a/packages/core/deprecations/core-deprecations-server-internal/README.md b/packages/core/deprecations/core-deprecations-server-internal/README.md index 8257dfb40ebbf..01b5b86014b47 100644 --- a/packages/core/deprecations/core-deprecations-server-internal/README.md +++ b/packages/core/deprecations/core-deprecations-server-internal/README.md @@ -1,3 +1,90 @@ # @kbn/core-deprecations-server-internal This package contains the internal types and implementation of Core's server-side `deprecations` service. + + +/** Router: + * register Usage counters side core setup + * DOMIANID: CORE.ROUTER + * At router definition + * Call deprecations.registerDeprecation + * - group all renamed etc + + * GroupId: Method / path + * when route is called + if deprecation is triggered based on rename/ path/ remove + - increment counter: GroupId, domainId, type: 'count' + + +set: ['body.a'], +unset: ['body.a'], + +{ + "deprecations": [ + { + "configPath": "xpack.reporting.roles.enabled", + "title": "The \"xpack.reporting.roles\" setting is deprecated", + "level": "warning", + "message": "The default mechanism for Reporting privileges will work differently in future versions, which will affect the behavior of this cluster. Set \"xpack.reporting.roles.enabled\" to \"false\" to adopt the future behavior before upgrading.", + "correctiveActions": { + "manualSteps": [ + "Set \"xpack.reporting.roles.enabled\" to \"false\" in kibana.yml.", + "Remove \"xpack.reporting.roles.allow\" in kibana.yml, if present.", + "Go to Management > Security > Roles to create one or more roles that grant the Kibana application privilege for Reporting.", + "Grant Reporting privileges to users by assigning one of the new roles." + ], + api: { + path: 'some-path', + method: 'POST', + body: { + extra_param: 123, + }, + }, + }, + "deprecationType": "config", + "requireRestart": true, + "domainId": "xpack.reporting" + } + ] +} + + +domainId: 'routesDeprecations' +counterName: '{RouteAPIGroupingID}', +counterType: 'count', + +RouteAPIGroupingID +If Route level: method, route + +For fixed: +counterType: 'count:fixed', + +We count all deprecations + +{ + 'RouteAPIGroupingID': { + message: '', + deprecationDetails + } +} + + +Approach 1: +In memory: +1. Store in memory the deprecation details defined at routes setup + +3. enrich some text (last called etc) filter out diff 0 +4. send result + +SO and get rid of Usage counter + +interface UsageCountersParams { + /** The domainId used to create the Counter API */ + domainId: string; + /** The name of the counter. Optional, will return all counters in the same domainId that match the rest of filters if omitted */ + counterName?: string; + /** The 2. on UA api call we do matching between usage counters and the deprecation details in memorytype of counter. Optional, will return all counters in the same domainId that match the rest of filters if omitted */ + counterType?: string; + /** Namespace of the counter. Optional, counters of the 'default' namespace will be returned if omitted */ + namespace?: string; +} diff --git a/packages/core/deprecations/core-deprecations-server-internal/src/deprecations_factory.ts b/packages/core/deprecations/core-deprecations-server-internal/src/deprecations_factory.ts index f6f5ebb0bfb81..132a7c24914a7 100644 --- a/packages/core/deprecations/core-deprecations-server-internal/src/deprecations_factory.ts +++ b/packages/core/deprecations/core-deprecations-server-internal/src/deprecations_factory.ts @@ -62,7 +62,10 @@ export class DeprecationsFactory { }) ); - return deprecationsInfo.flat(); + return [ + ...deprecationsInfo.flat(), + // ...apiDeprecations, + ]; }; private createDeprecationInfo = ( 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..f4ade4b6c2d28 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,14 @@ 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 './register_core_deprecations'; export interface InternalDeprecationsServiceStart { /** @@ -40,6 +45,7 @@ export type InternalDeprecationsServiceSetup = DeprecationRegistryProvider; /** @internal */ export interface DeprecationsSetupDeps { http: InternalHttpServiceSetup; + coreUsageData: InternalCoreUsageDataSetup; } /** @internal */ @@ -55,7 +61,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( @@ -70,7 +79,18 @@ export class DeprecationsService }); registerRoutes({ http }); - this.registerConfigDeprecationsInfo(this.deprecationsFactory); + + registerConfigDeprecationsInfo({ + deprecationsFactory: this.deprecationsFactory, + configService: this.configService, + }); + + registerApiDeprecationsInfo({ + deprecationsFactory: this.deprecationsFactory, + logger: this.logger, + http, + coreUsageData, + }); const deprecationsFactory = this.deprecationsFactory; return { @@ -87,6 +107,7 @@ export class DeprecationsService if (!this.deprecationsFactory) { throw new Error('`setup` must be called before `start`'); } + return { asScopedToClient: this.createScopedDeprecations(), }; @@ -107,35 +128,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/register_core_deprecations.ts b/packages/core/deprecations/core-deprecations-server-internal/src/register_core_deprecations.ts new file mode 100644 index 0000000000000..6ef69bec45dfb --- /dev/null +++ b/packages/core/deprecations/core-deprecations-server-internal/src/register_core_deprecations.ts @@ -0,0 +1,114 @@ +/* + * 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 type { Logger } from '@kbn/logging'; +import { InternalHttpServiceSetup } from '@kbn/core-http-server-internal'; +import { InternalCoreUsageDataSetup } from '@kbn/core-usage-data-base-server-internal'; +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, + }) + ); + }, + }); + } +}; + +export interface ApiDeprecationsServiceDeps { + logger: Logger; + deprecationsFactory: DeprecationsFactory; + http: InternalHttpServiceSetup; + coreUsageData: InternalCoreUsageDataSetup; +} + +export const registerApiDeprecationsInfo = ({ + deprecationsFactory, + http, + coreUsageData, +}: ApiDeprecationsServiceDeps): void => { + const deprecationsRegistery = deprecationsFactory.getRegistry('core.api_deprecations'); + + deprecationsRegistery.registerDeprecations({ + getDeprecations: async () => { + console.log('calling get!!'); + + const usageClient = coreUsageData.getClient(); + const deprecatedRoutes = http.getDeprecatedRoutes(); + const deprecatedStats = await usageClient.getDeprecatedApisStats(); + + console.log('deprecatedRoutes::', deprecatedRoutes); + console.log('deprecatedStats::', deprecatedStats); + + // Do the matching here + // Do the diff here + // write the messages here + + return [ + { + routePath: '/api/chocolate_love', + routeMethod: 'GET', + title: `The Route "[GET] /api/chocolate_love" has deprected params`, + level: 'warning', + message: `Deprecated route [GET] /api/chocolate_love was called 34 times with deprecated params.\n + The last time the deprecation was triggered was on Fri Sep 20 2024 14:28:22.\n + This deprecation was previously marked as resolved but was called 3 times since it was marked on Fri Sep 13 2024 10:28:22.`, + documentationUrl: 'https://google.com', + correctiveActions: { + manualSteps: [ + 'The following query params are deprecated: dont_use,my_old_query_param', + 'Make sure you are not using any of these parameters when calling the API', + ], + api: { + path: 'some-path', + method: 'POST', + body: { + extra_param: 123, + }, + }, + }, + deprecationType: 'api', + requireRestart: false, + domainId: 'core.router', + }, + ]; + }, + }); +}; diff --git a/packages/core/deprecations/core-deprecations-server-internal/src/routes/get.ts b/packages/core/deprecations/core-deprecations-server-internal/src/routes/get.ts index ed3cd061b633b..e987eb9e5e82b 100644 --- a/packages/core/deprecations/core-deprecations-server-internal/src/routes/get.ts +++ b/packages/core/deprecations/core-deprecations-server-internal/src/routes/get.ts @@ -15,6 +15,17 @@ export const registerGetRoute = (router: InternalDeprecationRouter) => { { path: '/', validate: false, + options: { + deprecated: { + guideLink: 'https://google.com', + severity: 'warning', + reason: { + type: 'migrated-api', + apiMethod: 'GET', + apiPath: '/api/core/deprecations', + }, + }, + }, }, async (context, req, res) => { const deprecationsClient = (await context.core).deprecations.client; 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 b7b23186def88..1ffacba7c567b 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 @@ -254,6 +254,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 || [], timeout: { 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 a6f2ccc35f56b..9b261bdcb44d2 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'; @@ -142,7 +143,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 */ @@ -162,12 +169,18 @@ interface InternalGetRoutesOptions { excludeVersionedRoutes?: boolean; } +/** @internal */ +type RouterEvents = + /** Just before registered handlers are called */ + 'onPostValidate'; + /** * @internal */ export class Router implements IRouter { + private static ee = new EventEmitter(); public routes: Array> = []; public pluginId?: symbol; public get: InternalRegistrar<'get', Context>; @@ -188,7 +201,10 @@ export class Router( route: RouteConfig, handler: RequestHandler, - internalOptions: { isVersioned: boolean } = { isVersioned: false } + internalOptions: InternalRegistrarOptions = { + isVersioned: false, + events: true, + } ) => { route = prepareRouteConfigValidation(route); const routeSchemas = routeSchemasFromRouteConfig(route, method); @@ -200,6 +216,9 @@ export class Router void) { + Router.ee.on(event, cb); + } + + public static off(event: RouterEvents, cb: (req: CoreKibanaRequest) => void) { + Router.ee.off(event, cb); + } + public getRoutes({ excludeVersionedRoutes }: InternalGetRoutesOptions = {}) { if (excludeVersionedRoutes) { return this.routes.filter((route) => !route.isVersioned); @@ -246,15 +273,25 @@ export class Router { + const postValidate: RouterEvents = 'onPostValidate'; + Router.ee.emit(postValidate, request); + }; + private async handle({ routeSchemas, request, responseToolkit, + emit, handler, }: { request: Request; responseToolkit: ResponseToolkit; handler: RequestHandlerEnhanced; + emit?: { + onPostValidation: (req: KibanaRequest) => void; + }; routeSchemas?: RouteValidator; }) { let kibanaRequest: KibanaRequest; @@ -266,6 +303,8 @@ export class Router HttpServerInfo; + + getDeprecatedRoutes: HttpServiceSetup['getDeprecatedRoutes']; } /** @internal */ @@ -202,6 +204,26 @@ export class HttpServer { return this.server !== undefined && this.server.listener.listening; } + private getDeprecatedRoutes() { + const deprecatedRoutes: any[] = []; + + for (const router of this.registeredRouters) { + for (const route of router.getRoutes()) { + if (isObject(route.options.deprecated)) { + deprecatedRoutes.push({ + basePath: router.routerPath, + routeVerion: 'v1', // TODO(Bamieh): ADD this to route + routePath: route.path, + routeOptions: route.options, + routeMethod: route.method, + }); + } + } + } + + return deprecatedRoutes; + } + private registerRouter(router: IRouter) { if (this.isListening()) { throw new Error('Routers can be registered only when HTTP server is stopped.'); @@ -281,6 +303,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, @@ -694,12 +717,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, }; // Log HTTP API target consumer. optionsLogger.debug( 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 e5a82f0abefb0..2861652976fa3 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 @@ -182,6 +182,14 @@ export class HttpService this.internalSetup = { ...serverContract, + registerOnPostValidation: (cb) => { + Router.on('onPostValidate', cb); + }, + + getRegisteredDeprecatedApis: () => { + return serverContract.getDeprecatedRoutes(); + }, + externalUrl: new ExternalUrlConfig(config.externalUrl), createRouter: ( @@ -208,7 +216,7 @@ export class HttpService ) => this.requestHandlerContext!.registerContext(pluginOpaqueId, contextName, provider), }; - return this.internalSetup; + return this.internalSetup!; } // this method exists because we need the start contract to create the `CoreStart` used to start 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..bd4160d4d4b67 100644 --- a/packages/core/http/core-http-server-internal/src/types.ts +++ b/packages/core/http/core-http-server-internal/src/types.ts @@ -17,6 +17,7 @@ import type { HttpServiceSetup, HttpServiceStart, } from '@kbn/core-http-server'; +import { CoreKibanaRequest } from '@kbn/core-http-router-server-internal'; import type { HttpServerSetup } from './http_server'; import type { ExternalUrlConfig } from './external_url'; import type { InternalStaticAssets } from './static_assets'; @@ -65,6 +66,8 @@ export interface InternalHttpServiceSetup contextName: ContextName, provider: IContextProvider ) => IContextContainer; + registerOnPostValidation(cb: (req: CoreKibanaRequest) => void): void; + getRegisteredDeprecatedApis: () => any[]; } /** @internal */ diff --git a/packages/core/http/core-http-server/index.ts b/packages/core/http/core-http-server/index.ts index 64387e5ca36d7..8ef3f8c0a85de 100644 --- a/packages/core/http/core-http-server/index.ts +++ b/packages/core/http/core-http-server/index.ts @@ -107,6 +107,13 @@ export type { RouteValidatorFullConfigResponse, LazyValidator, RouteAccess, + RouteDeprecation, + RouteInputDeprecationDescription, + RouteInputDeprecation, + RouteInputDeprecationFactory, + RouteInputDeprecationLocation, + RouteInputDeprecationRemovedDescription, + RouteInputDeprecationRenamedDescription, } from './src/router'; export { validBodyOutput, 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..81871b502bee0 100644 --- a/packages/core/http/core-http-server/src/http_contract.ts +++ b/packages/core/http/core-http-server/src/http_contract.ts @@ -359,6 +359,8 @@ export interface HttpServiceSetup< * Provides common {@link HttpServerInfo | information} about the running http server. */ getServerInfo: () => HttpServerInfo; + + getDeprecatedRoutes: () => any[]; } /** @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 89e9a345179b6..abb1b23b2c739 100644 --- a/packages/core/http/core-http-server/src/router/index.ts +++ b/packages/core/http/core-http-server/src/router/index.ts @@ -53,6 +53,13 @@ export type { RouteContentType, SafeRouteMethod, RouteAccess, + RouteDeprecation, + RouteInputDeprecation, + RouteInputDeprecationFactory, + RouteInputDeprecationLocation, + RouteInputDeprecationDescription, + RouteInputDeprecationRemovedDescription, + RouteInputDeprecationRenamedDescription, } from './route'; export { validBodyOutput } from './route'; export type { 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 9080c1be48c8c..3a9f3d695acd2 100644 --- a/packages/core/http/core-http-server/src/router/request.ts +++ b/packages/core/http/core-http-server/src/router/request.ts @@ -13,13 +13,14 @@ 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 } from './route'; +import type { RouteMethod, RouteConfigOptions, RouteDeprecation } from './route'; import type { Headers } from './headers'; /** * @public */ export interface KibanaRouteOptions extends RouteOptionsApp { + deprecated?: RouteDeprecation; xsrfRequired: boolean; access: 'internal' | 'public'; } 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 c47688b60d3cd..840465d39a63e 100644 --- a/packages/core/http/core-http-server/src/router/route.ts +++ b/packages/core/http/core-http-server/src/router/route.ts @@ -111,11 +111,98 @@ export interface RouteConfigOptionsBody { */ export type RouteAccess = 'public' | 'internal'; +/** @public */ +export type RouteInputDeprecationLocation = 'query' | 'body'; + +/** @public */ +export interface RouteInputDeprecationRenamedDescription { + type: 'renamed'; + location: RouteInputDeprecationLocation; + old: string; + new: string; +} + +/** @public */ +export interface RouteInputDeprecationRemovedDescription { + type: 'removed'; + location: RouteInputDeprecationLocation; + path: string; +} + +/** @public */ +export type RouteInputDeprecationDescription = + | RouteInputDeprecationRenamedDescription + | RouteInputDeprecationRemovedDescription; + +/** + * Factory for supported types of route API deprecations. + * @public + */ +export type RouteInputDeprecationFactory = (factories: { + remove(path: string): RouteInputDeprecationRemovedDescription; + rename(oldPath: string, newPath: string): RouteInputDeprecationRenamedDescription; +}) => RouteInputDeprecationDescription[]; + +interface NewRouteDeprecationType { + type: 'new-version'; + apiVersion: string; +} +interface RemovedApiDeprecationType { + type: 'removed'; +} +interface MigratedApiDeprecationType { + type: 'migrated-api'; + apiPath: string; + apiMethod: string; +} + +export type RouteInputDeprecationReason = + | NewRouteDeprecationType + | RemovedApiDeprecationType + | MigratedApiDeprecationType; + +/** + * Description of deprecations for this HTTP API. + * + * @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). + * + * boolean - Set this to `true` to specify that this entire route is deprecated. + * + * It's also possible to provide deprecation messages about sub-parts of the route. Consider this + * example of a route deprecating an enum value from its request body: + * + * ```ts + * { + * body: { + * foo: { + * type: { check: (v) => v === "bar", message : "'bar' is deprecated. Use 'qux' or 'baz' instead." } + * } + * } + * } + * ``` + * + * @default false + * @public + */ +export interface RouteDeprecation { + severity: 'warning' | 'critical'; + /** new version is only for versioned router, removed is a special case */ + reason: RouteInputDeprecationReason; + guideLink: string; +} + /** * Additional route options. * @public */ -export interface RouteConfigOptions { +export interface RouteConfigOptions< + Method extends RouteMethod, + P = unknown, + Q = unknown, + B = unknown +> { /** * Defines authentication mode for a route: * - true. A user has to have valid credentials to access a resource @@ -201,12 +288,11 @@ export interface RouteConfigOptions { description?: string; /** - * Setting this to `true` declares this route to be deprecated. Consumers SHOULD - * refrain from usage of this route. + * A description of this routes deprecations. * - * @remarks This will be surfaced in OAS documentation. + * @remarks This may be surfaced in OAS documentation. */ - deprecated?: boolean; + deprecated?: RouteDeprecation; /** * Release version or date that this route will be removed @@ -299,5 +385,5 @@ export interface RouteConfig { /** * Additional route options {@link RouteConfigOptions}. */ - options?: RouteConfigOptions; + options?: RouteConfigOptions; } diff --git a/packages/core/http/core-http-server/src/versioning/README.md b/packages/core/http/core-http-server/src/versioning/README.md deleted file mode 100644 index 60553d059ada9..0000000000000 --- a/packages/core/http/core-http-server/src/versioning/README.md +++ /dev/null @@ -1,11 +0,0 @@ -This folder contains types for sever-side HTTP versioning. - -## Experimental - -The types in this package are all experimental and may be subject to extensive changes. -Use this package as a reference for current thinking and as a starting point to -raise questions and discussion. - -## Versioning specification - -Currently the versioning spec is being designed. \ No newline at end of file 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 c552abd251a1f..dd5d3dd7ba1ab 100644 --- a/packages/core/http/core-http-server/src/versioning/types.ts +++ b/packages/core/http/core-http-server/src/versioning/types.ts @@ -19,12 +19,20 @@ import type { RequestHandlerContextBase, RouteValidationFunction, LazyValidator, + RouteInputDeprecation, } from '../..'; type RqCtx = RequestHandlerContextBase; export type { ApiVersion }; +/** + * It is better practice to provide a string that can be surfaced to end users + * guiding them to alternative routes. + * @public + */ +export type VersionedRouteDeprecation = boolean | string; + /** * Configuration for a versioned route * @public @@ -93,7 +101,7 @@ export type VersionedRouteConfig = Omit< * * @default false */ - deprecated?: boolean; + deprecated?: VersionedRouteDeprecation; /** * Release version or date that this route will be removed @@ -337,6 +345,11 @@ export interface AddVersionOpts { * @public */ validate: false | VersionedRouteValidation | (() => VersionedRouteValidation); // Provide a way to lazily load validation schemas + + /** + * A description of which parts of this route are deprecated. + */ + deprecated?: RouteInputDeprecation; } /** diff --git a/packages/core/root/core-root-server-internal/src/route_deprecations/index.ts b/packages/core/root/core-root-server-internal/src/route_deprecations/index.ts new file mode 100644 index 0000000000000..5729dff065a25 --- /dev/null +++ b/packages/core/root/core-root-server-internal/src/route_deprecations/index.ts @@ -0,0 +1,10 @@ +/* + * 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 { createRouteDeprecationsHandler } from './route_deprecations'; diff --git a/packages/core/root/core-root-server-internal/src/route_deprecations/route_deprecations.ts b/packages/core/root/core-root-server-internal/src/route_deprecations/route_deprecations.ts new file mode 100644 index 0000000000000..fdd28a11dc907 --- /dev/null +++ b/packages/core/root/core-root-server-internal/src/route_deprecations/route_deprecations.ts @@ -0,0 +1,42 @@ +/* + * 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 { Logger } from '@kbn/logging'; + +import { CoreKibanaRequest } from '@kbn/core-http-router-server-internal'; +import { isObject } from 'lodash'; + +interface Dependencies { + logRouteApiDeprecations: boolean; // TODO(jloleysens) use this + log: Logger; + coreUsageData: InternalCoreUsageDataSetup; +} + +export function createRouteDeprecationsHandler({ coreUsageData }: Dependencies) { + return (req: CoreKibanaRequest) => { + const { + route: { + options: { deprecated: deprecatedInput }, + }, + } = req; + + if (typeof deprecatedInput === 'boolean') { + console.log('INVALID DEPRECATION INPUT!!! GOT BOOLEAN'); + } + + if (isObject(deprecatedInput)) { + const routeVersion = 'v1'; + const counterName = `[${routeVersion}][${req.route.method}] ${req.route.path}`; + + console.log(`Incrementing deprecated route: ${req.route.path}`); + coreUsageData.incrementDeprecatedApiUsageCounter({ counterName }); + } + }; +} 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..6230b349b7b0f 100644 --- a/packages/core/root/core-root-server-internal/src/server.ts +++ b/packages/core/root/core-root-server-internal/src/server.ts @@ -59,6 +59,7 @@ import { registerServiceConfig } from './register_service_config'; import { MIGRATION_EXCEPTION_CODE } from './constants'; import { coreConfig, type CoreConfigType } from './core_config'; import { registerRootEvents, reportKibanaStartedEvent, type UptimeSteps } from './events'; +import { createRouteDeprecationsHandler } from './route_deprecations'; const coreId = Symbol('core'); @@ -100,6 +101,7 @@ export class Server { #pluginsInitialized?: boolean; private coreStart?: InternalCoreStart; + private coreSetup?: InternalCoreSetup; private discoveredPlugins?: DiscoveredPlugins; private readonly logger: LoggerFactory; private nodeRoles?: NodeRoles; @@ -276,10 +278,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 +301,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, @@ -378,6 +381,8 @@ export class Server { this.#pluginsInitialized = pluginsSetup.initialized; this.registerCoreContext(coreSetup); + this.registerApiRouteDeprecationsLogger(coreSetup); + await this.coreApp.setup(coreSetup, uiPlugins); setupTransaction.end(); @@ -530,6 +535,15 @@ export class Server { } ); } + private registerApiRouteDeprecationsLogger(coreSetup: InternalCoreSetup) { + coreSetup.http.registerOnPostValidation( + createRouteDeprecationsHandler({ + coreUsageData: coreSetup.coreUsageData, + log: this.logger.get('http.route-deprecations'), + logRouteApiDeprecations: true, + }) + ); + } public setupCoreConfig() { registerServiceConfig(this.configService); diff --git a/packages/core/usage-data/core-usage-data-base-server-internal/index.ts b/packages/core/usage-data/core-usage-data-base-server-internal/index.ts index aad2883b016a6..3e3651ad9e1c7 100644 --- a/packages/core/usage-data/core-usage-data-base-server-internal/index.ts +++ b/packages/core/usage-data/core-usage-data-base-server-internal/index.ts @@ -14,6 +14,7 @@ export { } from './src'; export type { InternalCoreUsageDataSetup, + IDeprecationsStatsClient, ICoreUsageStatsClient, BaseIncrementOptions, IncrementSavedObjectsImportOptions, diff --git a/packages/core/usage-data/core-usage-data-base-server-internal/src/internal_contract.ts b/packages/core/usage-data/core-usage-data-base-server-internal/src/internal_contract.ts index 4ed197817c277..17cea797156e2 100644 --- a/packages/core/usage-data/core-usage-data-base-server-internal/src/internal_contract.ts +++ b/packages/core/usage-data/core-usage-data-base-server-internal/src/internal_contract.ts @@ -23,4 +23,7 @@ export interface InternalCoreUsageDataSetup extends CoreUsageDataSetup { /** @internal {@link CoreIncrementUsageCounter} **/ incrementUsageCounter: CoreIncrementUsageCounter; + + /** @internal {@link CoreIncrementUsageCounter} **/ + incrementDeprecatedApiUsageCounter: CoreIncrementUsageCounter; } 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..106d450dc234e 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 @@ -38,6 +38,8 @@ export type IncrementSavedObjectsExportOptions = BaseIncrementOptions & { export interface ICoreUsageStatsClient { getUsageStats(): Promise; + getDeprecatedApisStats(): 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..bcbdd3756a531 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 @@ -88,6 +88,7 @@ export class CoreUsageDataService private coreUsageStatsClient?: CoreUsageStatsClient; private deprecatedConfigPaths: ChangedDeprecatedPaths = { set: [], unset: [] }; private incrementUsageCounter: CoreIncrementUsageCounter = () => {}; // Initially set to noop + private incrementDeprecatedApiUsageCounter: CoreIncrementUsageCounter = () => {}; // Initially set to noop constructor(core: CoreContext) { this.logger = core.logger.get('core-usage-stats-service'); @@ -502,6 +503,9 @@ export class CoreUsageDataService const registerUsageCounter = (usageCounter: CoreUsageCounter) => { this.incrementUsageCounter = (params) => usageCounter.incrementCounter(params); }; + const registerDeprecatedApiUsageCounter = (usageCounter: CoreUsageCounter) => { + this.incrementDeprecatedApiUsageCounter = (params) => usageCounter.incrementCounter(params); + }; const incrementUsageCounter = (params: CoreIncrementCounterParams) => { try { @@ -513,6 +517,16 @@ export class CoreUsageDataService } }; + const incrementDeprecatedApiUsageCounter = (params: CoreIncrementCounterParams) => { + try { + this.incrementDeprecatedApiUsageCounter(params); + } catch (e) { + // Self-defense mechanism since the handler is externally registered + this.logger.debug('Failed to increase the usage counter'); + this.logger.debug(e); + } + }; + this.coreUsageStatsClient = new CoreUsageStatsClient({ debugLogger: (message: string) => this.logger.debug(message), basePath: http.basePath, @@ -524,8 +538,12 @@ export class CoreUsageDataService const contract: InternalCoreUsageDataSetup = { registerType, getClient: () => this.coreUsageStatsClient!, + // Core usage stats registerUsageCounter, incrementUsageCounter, + // api deprecations + registerDeprecatedApiUsageCounter, + incrementDeprecatedApiUsageCounter, }; return contract; 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..f25b3fd7eb640 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 @@ -215,6 +215,30 @@ export class CoreUsageStatsClient implements ICoreUsageStatsClient { return coreUsageStats; } + public async getDeprecatedApisStats(): Promise { + this.debugLogger('getDeprecatedApisStats() called'); + const coreUsageStats: any[] = []; + try { + const repository = await this.repositoryPromise; + this.flush$.next(); + // const result = await repository.incrementCounter( + // CORE_USAGE_STATS_TYPE, + // CORE_USAGE_STATS_ID, + // ALL_COUNTER_FIELDS, + // { initialize: true } // set all counter fields to 0 if they don't exist + // ); + coreUsageStats.push({ + counterName: '[v1][GET] /api/chocolate_love', + count: 4, + type: 'count', + lastUpdated: new Date(), + }); + } catch (err) { + // do nothing + } + return coreUsageStats; + } + public async incrementSavedObjectsBulkCreate(options: BaseIncrementOptions) { await this.updateUsageStats([], BULK_CREATE_STATS_PREFIX, options); } 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..b115a8cf2e24b 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', } ), + routeDeprecationTypeCellLabel: i18n.translate( + 'xpack.upgradeAssistant.kibanaDeprecations.table.routeDeprecationTypeCellLabel', + { + defaultMessage: 'Route', + } + ), unknownDeprecationTypeCellLabel: i18n.translate( 'xpack.upgradeAssistant.kibanaDeprecations.table.unknownDeprecationTypeCellLabel', { @@ -135,6 +141,8 @@ export const KibanaDeprecationsTable: React.FunctionComponent = ({ return i18nTexts.configDeprecationTypeCellLabel; case 'feature': return i18nTexts.featureDeprecationTypeCellLabel; + case 'route': + return i18nTexts.routeDeprecationTypeCellLabel; case 'uncategorized': default: return i18nTexts.unknownDeprecationTypeCellLabel; diff --git a/x-pack/plugins/upgrade_assistant/server/plugin.ts b/x-pack/plugins/upgrade_assistant/server/plugin.ts index 3df9f7deced9d..7610e6f950680 100644 --- a/x-pack/plugins/upgrade_assistant/server/plugin.ts +++ b/x-pack/plugins/upgrade_assistant/server/plugin.ts @@ -103,6 +103,14 @@ export class UpgradeAssistantServerPlugin implements Plugin { ], }); + http.registerOnPreResponse(async (req, res, t) => { + if (req.route.options.deprecated) { + // console.log('route!') + } + + return t.next(); + }); + // We need to initialize the deprecation logs plugin so that we can // navigate from this app to the observability app using a source_id. logsShared?.logViews.defineInternalLogView(DEPRECATION_LOGS_SOURCE_ID, {