[UA][Core] Surface integrations with internal APIs in upgrade assistant (#199026)

## Summary

> In https://github.com/elastic/kibana/issues/117241 we're surfacing
usage of APIs marked as `deprecated: true` in the Upgrade Assistant to
help users prepare for a major upgrade. While internal APIs aren't
really deprecated in the same sense we are making a breaking change by
blocking external integrations with these APIs. Since this could be
equally disruptive to users depending on these APIs it would help our
users to surface such usage in the UA too.

The `api` deprecations now have two sub types:
1. routes deprecations `options.deprecated: { … }`
2. access deprecations `options.access: 'internal'`

This PR adds the second `api` deprecation subtype. The reason i kept one
`api` deprecation type and i didnt create a new type is that they have
exactly the same registration process but are triggered by different
attributes. The `api` deprecation is fully managed by the core team
internal services and are configured by the user through the route
interface so it makes sense to keep them as one type. I also can see us
adding more subtypes to this and just piggybacking on the current flow
instead of duplicating it everytime.


**Checklist**
- [x] Create deprecation subtype
- [x] Example plugin
- [x] Surface the deprecation in UA
- [x] Api access deprecation copy (@florent-leborgne )
- [x] Update README and code annotations
- [x] Unit tests
- [x] Integration tests


Closes https://github.com/elastic/kibana/issues/194675

### Design decisions:
If the API has both route deprecation (`options.deprecated: { … }` ) AND
is an internal api `options.access: 'internal'`

The current behavior i went for in my PR:
I show this API once in the UA under the internal access deprecation.
While showing the route deprecation details if defined. This seems to
make the most sense since users should stop using this API altogether.

### Copy decisions:
@florent-leborgne wrote the copy for this deprecation subtype.
<img width="1319" alt="image"
src="https://github.com/user-attachments/assets/9a32f6d1-686a-4405-aec6-786ac5e10130">

<img width="713" alt="image"
src="https://github.com/user-attachments/assets/1304c98d-4c64-468e-a7d6-19c1193bf678">


## Testing

Run kibana locally with the test example plugin that has deprecated
routes
```
yarn start --plugin-path=examples/routing_example --plugin-path=examples/developer_examples
```

The following comprehensive deprecated routes examples are registered
inside the folder:
`examples/routing_example/server/routes/deprecated_routes`

Run them in the dev console to trigger the deprecation condition so they
show up in the UA:

```
GET kbn:/api/routing_example/d/internal_deprecated_route?elasticInternalOrigin=false
GET kbn:/internal/routing_example/d/internal_only_route?elasticInternalOrigin=false
GET kbn:/internal/routing_example/d/internal_versioned_route?apiVersion=1&elasticInternalOrigin=false
```

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Ahmad Bamieh 2024-11-12 14:19:22 +03:00 committed by GitHub
parent fb666aa765
commit a10eb1fe4e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
41 changed files with 916 additions and 307 deletions

View file

@ -20,5 +20,8 @@ export const DEPRECATED_ROUTES = {
DEPRECATED_ROUTE: '/api/routing_example/d/deprecated_route',
REMOVED_ROUTE: '/api/routing_example/d/removed_route',
MIGRATED_ROUTE: '/api/routing_example/d/migrated_route',
VERSIONED_ROUTE: '/api/routing_example/d/versioned',
VERSIONED_ROUTE: '/api/routing_example/d/versioned_route',
INTERNAL_DEPRECATED_ROUTE: '/api/routing_example/d/internal_deprecated_route',
INTERNAL_ONLY_ROUTE: '/internal/routing_example/d/internal_only_route',
VERSIONED_INTERNAL_ROUTE: '/internal/routing_example/d/internal_versioned_route',
};

View file

@ -10,8 +10,10 @@
import { IRouter } from '@kbn/core/server';
import { registerDeprecatedRoute } from './unversioned';
import { registerVersionedDeprecatedRoute } from './versioned';
import { registerInternalDeprecatedRoute } from './internal';
export function registerDeprecatedRoutes(router: IRouter) {
registerDeprecatedRoute(router);
registerVersionedDeprecatedRoute(router);
registerInternalDeprecatedRoute(router);
}

View file

@ -0,0 +1,54 @@
/*
* 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 { DEPRECATED_ROUTES } from '../../../common';
export const registerInternalDeprecatedRoute = (router: IRouter) => {
router.get(
{
path: DEPRECATED_ROUTES.INTERNAL_DEPRECATED_ROUTE,
validate: false,
options: {
// Explicitly set access is to internal
access: 'internal',
deprecated: {
documentationUrl: 'https://elastic.co/',
severity: 'critical',
message: 'Additonal message for internal deprecated api',
reason: { type: 'deprecate' },
},
},
},
async (ctx, req, res) => {
return res.ok({
body: {
result:
'Called deprecated route with `access: internal`. Check UA to see the deprecation.',
},
});
}
);
router.get(
{
path: DEPRECATED_ROUTES.INTERNAL_ONLY_ROUTE,
validate: false,
// If no access is specified then it defaults to internal
},
async (ctx, req, res) => {
return res.ok({
body: {
result:
'Called route with `access: internal` Although this API is not marked as deprecated it will show in UA. Check UA to see the deprecation.',
},
});
}
);
};

View file

@ -11,42 +11,72 @@ import type { IRouter } from '@kbn/core/server';
import { DEPRECATED_ROUTES } from '../../../common';
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(
{
router.versioned
.get({
path: DEPRECATED_ROUTES.VERSIONED_ROUTE,
description: 'Routing example plugin deprecated versioned route.',
access: 'public',
options: {
deprecated: {
documentationUrl: 'https://elastic.co/',
severity: 'warning',
reason: { type: 'bump', newApiVersion: '2' },
},
excludeFromOAS: true,
},
validate: false,
version: '1',
},
(ctx, req, res) => {
return res.ok({
body: { result: 'Called deprecated version of the API. API version 1 -> 2' },
});
}
);
enableQueryVersion: true,
})
.addVersion(
{
options: {
deprecated: {
documentationUrl: 'https://elastic.co/',
severity: 'warning',
reason: { type: 'deprecate' },
},
},
validate: false,
version: '2023-10-31',
},
(ctx, req, res) => {
return res.ok({
body: { result: 'Called deprecated version of the API "2023-10-31"' },
});
}
);
versionedRoute.addVersion(
{
version: '2',
validate: false,
},
(ctx, req, res) => {
return res.ok({ body: { result: 'Called API version 2' } });
}
);
router.versioned
.get({
path: DEPRECATED_ROUTES.VERSIONED_INTERNAL_ROUTE,
description: 'Routing example plugin deprecated versioned route.',
access: 'internal',
options: {
excludeFromOAS: true,
},
enableQueryVersion: true,
})
.addVersion(
{
options: {
deprecated: {
documentationUrl: 'https://elastic.co/',
severity: 'warning',
reason: { type: 'bump', newApiVersion: '2' },
},
},
validate: false,
version: '1',
},
(ctx, req, res) => {
return res.ok({
body: { result: 'Called internal deprecated version of the API 1.' },
});
}
)
.addVersion(
{
validate: false,
version: '2',
},
(ctx, req, res) => {
return res.ok({
body: { result: 'Called non-deprecated version of the API.' },
});
}
);
};

View file

@ -63,6 +63,9 @@ export function registerGetMessageByIdRoute(router: IRouter) {
router.get(
{
path: `${INTERNAL_GET_MESSAGE_BY_ID_ROUTE}/{id}`,
options: {
access: 'internal',
},
validate: {
params: schema.object({
id: schema.string(),

View file

@ -11,6 +11,7 @@ export type {
BaseDeprecationDetails,
ConfigDeprecationDetails,
FeatureDeprecationDetails,
ApiDeprecationDetails,
DeprecationsDetails,
DomainDeprecationDetails,
DeprecationsGetResponse,

View file

@ -121,7 +121,7 @@ export type DeprecationsDetails =
/**
* @public
*/
export type DomainDeprecationDetails = DeprecationsDetails & {
export type DomainDeprecationDetails<ExtendedDetails = DeprecationsDetails> = ExtendedDetails & {
domainId: string;
};

View file

@ -1,96 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import type { InternalHttpServiceSetup } from '@kbn/core-http-server-internal';
import type { InternalCoreUsageDataSetup } from '@kbn/core-usage-data-base-server-internal';
import type { RouterDeprecatedRouteDetails } from '@kbn/core-http-server';
import { DeprecationsDetails } from '@kbn/core-deprecations-common';
import type { DeprecationsFactory } from '../deprecations_factory';
import {
getApiDeprecationMessage,
getApiDeprecationsManualSteps,
getApiDeprecationTitle,
} from './i18n_texts';
interface ApiDeprecationsServiceDeps {
deprecationsFactory: DeprecationsFactory;
http: InternalHttpServiceSetup;
coreUsageData: InternalCoreUsageDataSetup;
}
export const buildApiDeprecationId = ({
routePath,
routeMethod,
routeVersion,
}: Pick<RouterDeprecatedRouteDetails, 'routeMethod' | 'routePath' | 'routeVersion'>): string => {
return [
routeVersion || 'unversioned',
routeMethod.toLocaleLowerCase(),
routePath.replace(/\/$/, ''),
].join('|');
};
export const createGetApiDeprecations =
({ http, coreUsageData }: Pick<ApiDeprecationsServiceDeps, 'coreUsageData' | 'http'>) =>
async (): Promise<DeprecationsDetails[]> => {
const deprecatedRoutes = http.getRegisteredDeprecatedApis();
const usageClient = coreUsageData.getClient();
const deprecatedApiUsageStats = await usageClient.getDeprecatedApiUsageStats();
return deprecatedApiUsageStats
.filter(({ apiTotalCalls, totalMarkedAsResolved }) => {
return apiTotalCalls > totalMarkedAsResolved;
})
.filter(({ apiId }) =>
deprecatedRoutes.some((routeDetails) => buildApiDeprecationId(routeDetails) === apiId)
)
.map((apiUsageStats) => {
const { apiId, apiTotalCalls, totalMarkedAsResolved } = apiUsageStats;
const routeDeprecationDetails = deprecatedRoutes.find(
(routeDetails) => buildApiDeprecationId(routeDetails) === apiId
)!;
const { routeVersion, routePath, routeDeprecationOptions, routeMethod } =
routeDeprecationDetails;
const deprecationLevel = routeDeprecationOptions.severity || 'warning';
return {
apiId,
title: getApiDeprecationTitle(routeDeprecationDetails),
level: deprecationLevel,
message: getApiDeprecationMessage(routeDeprecationDetails, apiUsageStats),
documentationUrl: routeDeprecationOptions.documentationUrl,
correctiveActions: {
manualSteps: getApiDeprecationsManualSteps(routeDeprecationDetails),
mark_as_resolved_api: {
routePath,
routeMethod,
routeVersion,
apiTotalCalls,
totalMarkedAsResolved,
timestamp: new Date(),
},
},
deprecationType: 'api',
domainId: 'core.routes-deprecations',
};
});
};
export const registerApiDeprecationsInfo = ({
deprecationsFactory,
http,
coreUsageData,
}: ApiDeprecationsServiceDeps): void => {
const deprecationsRegistery = deprecationsFactory.getRegistry('core.api_deprecations');
deprecationsRegistery.registerDeprecations({
getDeprecations: createGetApiDeprecations({ http, coreUsageData }),
});
};

View file

@ -0,0 +1,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 {
ApiDeprecationDetails,
DomainDeprecationDetails,
} from '@kbn/core-deprecations-common';
import type { PostValidationMetadata } from '@kbn/core-http-server';
import type { BuildApiDeprecationDetailsParams } from '../types';
import {
getApiDeprecationMessage,
getApiDeprecationsManualSteps,
getApiDeprecationTitle,
} from './i18n_texts';
export const getIsAccessApiDeprecation = ({
isInternalApiRequest,
isPublicAccess,
}: PostValidationMetadata): boolean => {
const isNotPublicAccess = !isPublicAccess;
const isNotInternalRequest = !isInternalApiRequest;
return !!(isNotPublicAccess && isNotInternalRequest);
};
export const buildApiAccessDeprecationDetails = ({
apiUsageStats,
deprecatedApiDetails,
}: BuildApiDeprecationDetailsParams): DomainDeprecationDetails<ApiDeprecationDetails> => {
const { apiId, apiTotalCalls, totalMarkedAsResolved } = apiUsageStats;
const { routeVersion, routePath, routeDeprecationOptions, routeMethod } = deprecatedApiDetails;
const deprecationLevel = routeDeprecationOptions?.severity || 'warning';
return {
apiId,
title: getApiDeprecationTitle(deprecatedApiDetails),
level: deprecationLevel,
message: getApiDeprecationMessage(deprecatedApiDetails, apiUsageStats),
documentationUrl: routeDeprecationOptions?.documentationUrl,
correctiveActions: {
manualSteps: getApiDeprecationsManualSteps(),
mark_as_resolved_api: {
routePath,
routeMethod,
routeVersion,
apiTotalCalls,
totalMarkedAsResolved,
timestamp: new Date(),
},
},
deprecationType: 'api',
domainId: 'core.http.access-deprecations',
};
};

View file

@ -0,0 +1,101 @@
/*
* 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 { RouterDeprecatedApiDetails } 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: Pick<RouterDeprecatedApiDetails, 'routePath' | 'routeMethod'>
) => {
const { routePath, routeMethod } = details;
const routeWithMethod = `${routeMethod.toUpperCase()} ${routePath}`;
return i18n.translate('core.deprecations.apiAccessDeprecation.infoTitle', {
defaultMessage: 'The "{routeWithMethod}" API is internal to Elastic',
values: {
routeWithMethod,
},
});
};
export const getApiDeprecationMessage = (
details: Pick<
RouterDeprecatedApiDetails,
'routePath' | 'routeMethod' | 'routeDeprecationOptions'
>,
apiUsageStats: CoreDeprecatedApiUsageStats
): string[] => {
const { routePath, routeMethod, routeDeprecationOptions } = 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.apiAccessDeprecation.apiCallsDetailsMessage', {
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.apiAccessDeprecation.previouslyMarkedAsResolvedMessage', {
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'),
},
})
);
}
messages.push(
i18n.translate('core.deprecations.apiAccessDeprecation.internalApiExplanationMessage', {
defaultMessage:
'Internal APIs are meant to be used by Elastic services only. You should not use them. External access to these APIs will be restricted.',
})
);
if (routeDeprecationOptions?.message) {
// Surfaces additional deprecation messages passed into the route in UA
messages.push(routeDeprecationOptions.message);
}
return messages;
};
export const getApiDeprecationsManualSteps = (): string[] => {
return [
i18n.translate('core.deprecations.apiAccessDeprecation.manualSteps.identifyCallsOriginStep', {
defaultMessage: 'Identify the origin of these API calls.',
}),
i18n.translate('core.deprecations.apiAccessDeprecation.manualSteps.deleteRequestsStep', {
defaultMessage:
'Delete any requests you have that use this API. Check the learn more link for possible alternatives.',
}),
i18n.translate(
'core.deprecations.apiAccessDeprecation.manualSteps.accessDepractionMarkAsResolvedStep',
{
defaultMessage:
'Once you have successfully stopped using this API, mark this issue as resolved. It will no longer appear in the Upgrade Assistant unless another call using this API is detected.',
}
),
];
};

View file

@ -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 { buildApiAccessDeprecationDetails, getIsAccessApiDeprecation } from './access_deprecations';

View file

@ -0,0 +1,51 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { buildApiDeprecationId } from './api_deprecation_id';
describe('#buildApiDeprecationId', () => {
it('returns apiDeprecationId string for versioned routes', () => {
const apiDeprecationId = buildApiDeprecationId({
routeMethod: 'get',
routePath: '/api/test',
routeVersion: '10-10-2023',
});
expect(apiDeprecationId).toBe('10-10-2023|get|/api/test');
});
it('returns apiDeprecationId string for unversioned routes', () => {
const apiDeprecationId = buildApiDeprecationId({
routeMethod: 'get',
routePath: '/api/test',
});
expect(apiDeprecationId).toBe('unversioned|get|/api/test');
});
it('gives the same ID the route method is capitalized or not', () => {
const apiDeprecationId = buildApiDeprecationId({
// @ts-expect-error
routeMethod: 'GeT',
routePath: '/api/test',
routeVersion: '10-10-2023',
});
expect(apiDeprecationId).toBe('10-10-2023|get|/api/test');
});
it('gives the same ID the route path has a trailing slash or not', () => {
const apiDeprecationId = buildApiDeprecationId({
// @ts-expect-error
routeMethod: 'GeT',
routePath: '/api/test/',
routeVersion: '10-10-2023',
});
expect(apiDeprecationId).toBe('10-10-2023|get|/api/test');
});
});

View file

@ -0,0 +1,22 @@
/*
* 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 { RouterDeprecatedApiDetails } from '@kbn/core-http-server';
export const buildApiDeprecationId = ({
routePath,
routeMethod,
routeVersion,
}: Pick<RouterDeprecatedApiDetails, 'routeMethod' | 'routePath' | 'routeVersion'>): string => {
return [
routeVersion || 'unversioned',
routeMethod.toLocaleLowerCase(),
routePath.replace(/\/$/, ''),
].join('|');
};

View file

@ -0,0 +1,13 @@
/*
* 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 { registerApiDeprecationsInfo } from './register_api_depercation_info';
export { getIsAccessApiDeprecation } from './access';
export { getIsRouteApiDeprecation } from './route';
export { buildApiDeprecationId } from './api_deprecation_id';

View file

@ -8,13 +8,13 @@
*/
import type { DeepPartial } from '@kbn/utility-types';
import { mockDeprecationsRegistry, mockDeprecationsFactory } from '../mocks';
import { mockDeprecationsRegistry, mockDeprecationsFactory } from '../../mocks';
import {
registerApiDeprecationsInfo,
buildApiDeprecationId,
createGetApiDeprecations,
} from './api_deprecations';
import { RouterDeprecatedRouteDetails } from '@kbn/core-http-server';
} from './register_api_depercation_info';
import { buildApiDeprecationId } from './api_deprecation_id';
import { RouterDeprecatedApiDetails } from '@kbn/core-http-server';
import { httpServiceMock } from '@kbn/core-http-server-mocks';
import {
coreUsageDataServiceMock,
@ -58,10 +58,11 @@ describe('#registerApiDeprecationsInfo', () => {
describe('#createGetApiDeprecations', () => {
const createDeprecatedRouteDetails = (
overrides?: DeepPartial<RouterDeprecatedRouteDetails>
): RouterDeprecatedRouteDetails =>
overrides?: DeepPartial<RouterDeprecatedApiDetails>
): RouterDeprecatedApiDetails =>
_.merge(
{
routeAccess: 'public',
routeDeprecationOptions: {
documentationUrl: 'https://fake-url',
severity: 'critical',
@ -72,7 +73,7 @@ describe('#registerApiDeprecationsInfo', () => {
routeMethod: 'get',
routePath: '/api/test/',
routeVersion: '123',
} as RouterDeprecatedRouteDetails,
} as RouterDeprecatedApiDetails,
overrides
);
@ -124,7 +125,7 @@ describe('#registerApiDeprecationsInfo', () => {
},
"deprecationType": "api",
"documentationUrl": "https://fake-url",
"domainId": "core.routes-deprecations",
"domainId": "core.http.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.",
@ -171,7 +172,7 @@ describe('#registerApiDeprecationsInfo', () => {
},
"deprecationType": "api",
"documentationUrl": "https://fake-url",
"domainId": "core.routes-deprecations",
"domainId": "core.http.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.",
@ -216,7 +217,7 @@ describe('#registerApiDeprecationsInfo', () => {
},
"deprecationType": "api",
"documentationUrl": "https://fake-url",
"domainId": "core.routes-deprecations",
"domainId": "core.http.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.",
@ -260,7 +261,7 @@ describe('#registerApiDeprecationsInfo', () => {
},
"deprecationType": "api",
"documentationUrl": "https://fake-url",
"domainId": "core.routes-deprecations",
"domainId": "core.http.routes-deprecations",
"level": "critical",
"message": Array [
"The API \\"GET /api/test_deprecated/\\" has been called 13 times. The last call was on Sunday, September 1, 2024 6:06 AM -04:00.",
@ -323,7 +324,7 @@ describe('#registerApiDeprecationsInfo', () => {
},
"deprecationType": "api",
"documentationUrl": "https://fake-url",
"domainId": "core.routes-deprecations",
"domainId": "core.http.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.",
@ -355,44 +356,3 @@ describe('#registerApiDeprecationsInfo', () => {
});
});
});
describe('#buildApiDeprecationId', () => {
it('returns apiDeprecationId string for versioned routes', () => {
const apiDeprecationId = buildApiDeprecationId({
routeMethod: 'get',
routePath: '/api/test',
routeVersion: '10-10-2023',
});
expect(apiDeprecationId).toBe('10-10-2023|get|/api/test');
});
it('returns apiDeprecationId string for unversioned routes', () => {
const apiDeprecationId = buildApiDeprecationId({
routeMethod: 'get',
routePath: '/api/test',
});
expect(apiDeprecationId).toBe('unversioned|get|/api/test');
});
it('gives the same ID the route method is capitalized or not', () => {
const apiDeprecationId = buildApiDeprecationId({
// @ts-expect-error
routeMethod: 'GeT',
routePath: '/api/test',
routeVersion: '10-10-2023',
});
expect(apiDeprecationId).toBe('10-10-2023|get|/api/test');
});
it('gives the same ID the route path has a trailing slash or not', () => {
const apiDeprecationId = buildApiDeprecationId({
// @ts-expect-error
routeMethod: 'GeT',
routePath: '/api/test/',
routeVersion: '10-10-2023',
});
expect(apiDeprecationId).toBe('10-10-2023|get|/api/test');
});
});

View file

@ -0,0 +1,70 @@
/*
* 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 { DeprecationsDetails } from '@kbn/core-deprecations-common';
import { buildApiRouteDeprecationDetails } from './route/route_deprecations';
import { buildApiAccessDeprecationDetails } from './access/access_deprecations';
import { buildApiDeprecationId } from './api_deprecation_id';
import type { ApiDeprecationsServiceDeps } from './types';
export const createGetApiDeprecations =
({ http, coreUsageData }: Pick<ApiDeprecationsServiceDeps, 'coreUsageData' | 'http'>) =>
async (): Promise<DeprecationsDetails[]> => {
const usageClient = coreUsageData.getClient();
const deprecatedApis = http.getRegisteredDeprecatedApis();
const deprecatedApiUsageStats = await usageClient.getDeprecatedApiUsageStats();
return deprecatedApiUsageStats
.filter(({ apiTotalCalls, totalMarkedAsResolved }) => {
return apiTotalCalls > totalMarkedAsResolved;
})
.filter(({ apiId }) =>
deprecatedApis.some((routeDetails) => buildApiDeprecationId(routeDetails) === apiId)
)
.map((apiUsageStats) => {
const { apiId } = apiUsageStats;
const deprecatedApiDetails = deprecatedApis.find(
(routeDetails) => buildApiDeprecationId(routeDetails) === apiId
);
if (!deprecatedApiDetails) {
throw new Error(`Unable to find deprecation details for "${apiId}"`);
}
const { routeAccess } = deprecatedApiDetails;
switch (routeAccess) {
case 'public': {
return buildApiRouteDeprecationDetails({
apiUsageStats,
deprecatedApiDetails,
});
}
// if no access is specified then internal is the default
case 'internal':
default: {
return buildApiAccessDeprecationDetails({
apiUsageStats,
deprecatedApiDetails,
});
}
}
});
};
export const registerApiDeprecationsInfo = ({
deprecationsFactory,
http,
coreUsageData,
}: ApiDeprecationsServiceDeps): void => {
const deprecationsRegistery = deprecationsFactory.getRegistry('core.api_deprecations');
deprecationsRegistery.registerDeprecations({
getDeprecations: createGetApiDeprecations({ http, coreUsageData }),
});
};

View file

@ -7,22 +7,26 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { RouterDeprecatedRouteDetails } from '@kbn/core-http-server';
import { RouterDeprecatedApiDetails } 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) => {
export const getApiDeprecationTitle = (details: RouterDeprecatedApiDetails) => {
const { routePath, routeMethod, routeDeprecationOptions } = details;
if (!routeDeprecationOptions) {
throw new Error(`Router "deprecated" param is missing for path "${routePath}".`);
}
const deprecationType = routeDeprecationOptions.reason.type;
const routeWithMethod = `${routeMethod.toUpperCase()} ${routePath}`;
const deprecationTypeText = i18n.translate('core.deprecations.deprecations.apiDeprecationType', {
const deprecationTypeText = i18n.translate('core.deprecations.apiRouteDeprecation.type', {
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', {
return i18n.translate('core.deprecations.apiRouteDeprecation.infoTitle', {
defaultMessage: 'The "{routeWithMethod}" route {deprecationTypeText}',
values: {
routeWithMethod,
@ -32,10 +36,13 @@ export const getApiDeprecationTitle = (details: RouterDeprecatedRouteDetails) =>
};
export const getApiDeprecationMessage = (
details: RouterDeprecatedRouteDetails,
details: RouterDeprecatedApiDetails,
apiUsageStats: CoreDeprecatedApiUsageStats
): string[] => {
const { routePath, routeMethod, routeDeprecationOptions } = details;
if (!routeDeprecationOptions) {
throw new Error(`Router "deprecated" param is missing for path "${routePath}".`);
}
const { apiLastCalledAt, apiTotalCalls, markedAsResolvedLastCalledAt, totalMarkedAsResolved } =
apiUsageStats;
@ -44,7 +51,7 @@ export const getApiDeprecationMessage = (
const routeWithMethod = `${routeMethod.toUpperCase()} ${routePath}`;
const messages = [
i18n.translate('core.deprecations.deprecations.apiDeprecationApiCallsDetailsMessage', {
i18n.translate('core.deprecations.apiRouteDeprecation.apiCallsDetailsMessage', {
defaultMessage:
'The API "{routeWithMethod}" has been called {apiTotalCalls} times. The last call was on {apiLastCalledAt}.',
values: {
@ -57,17 +64,14 @@ export const getApiDeprecationMessage = (
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'),
},
}
)
i18n.translate('core.deprecations.apiRouteDeprecation.previouslyMarkedAsResolvedMessage', {
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'),
},
})
);
}
@ -79,12 +83,16 @@ export const getApiDeprecationMessage = (
return messages;
};
export const getApiDeprecationsManualSteps = (details: RouterDeprecatedRouteDetails): string[] => {
const { routeDeprecationOptions } = details;
export const getApiDeprecationsManualSteps = (details: RouterDeprecatedApiDetails): string[] => {
const { routePath, routeDeprecationOptions } = details;
if (!routeDeprecationOptions) {
throw new Error(`Router "deprecated" param is missing for path "${routePath}".`);
}
const deprecationType = routeDeprecationOptions.reason.type;
const manualSteps = [
i18n.translate('core.deprecations.deprecations.manualSteps.apiIseprecatedStep', {
i18n.translate('core.deprecations.apiRouteDeprecation.manualSteps.identifyCallsOriginStep', {
defaultMessage: 'Identify the origin of these API calls.',
}),
];
@ -93,7 +101,7 @@ export const getApiDeprecationsManualSteps = (details: RouterDeprecatedRouteDeta
case 'bump': {
const { newApiVersion } = routeDeprecationOptions.reason;
manualSteps.push(
i18n.translate('core.deprecations.deprecations.manualSteps.bumpDetailsStep', {
i18n.translate('core.deprecations.apiRouteDeprecation.manualSteps.bumpTypeStep', {
defaultMessage:
'Update the requests to use the following new version of the API instead: "{newApiVersion}".',
values: { newApiVersion },
@ -104,7 +112,7 @@ export const getApiDeprecationsManualSteps = (details: RouterDeprecatedRouteDeta
case 'remove': {
manualSteps.push(
i18n.translate('core.deprecations.deprecations.manualSteps.removeTypeExplainationStep', {
i18n.translate('core.deprecations.apiRouteDeprecation.manualSteps.removeTypeStep', {
defaultMessage:
'This API no longer exists and no replacement is available. Delete any requests you have that use this API.',
})
@ -113,7 +121,7 @@ export const getApiDeprecationsManualSteps = (details: RouterDeprecatedRouteDeta
}
case 'deprecate': {
manualSteps.push(
i18n.translate('core.deprecations.deprecations.manualSteps.removeTypeExplainationStep', {
i18n.translate('core.deprecations.apiRouteDeprecation.manualSteps.deprecateTypeStep', {
defaultMessage:
'For now, the API will still work, but will be moved or removed in a future version. Check the Learn more link for more information. If you are no longer using the API, you can mark this issue as resolved. It will no longer appear in the Upgrade Assistant unless another call using this API is detected.',
})
@ -125,7 +133,7 @@ export const getApiDeprecationsManualSteps = (details: RouterDeprecatedRouteDeta
const newRouteWithMethod = `${newApiMethod.toUpperCase()} ${newApiPath}`;
manualSteps.push(
i18n.translate('core.deprecations.deprecations.manualSteps.migrateDetailsStep', {
i18n.translate('core.deprecations.apiRouteDeprecation.manualSteps.migrateTypeStep', {
defaultMessage:
'Update the requests to use the following new API instead: "{newRouteWithMethod}".',
values: { newRouteWithMethod },
@ -137,10 +145,13 @@ export const getApiDeprecationsManualSteps = (details: RouterDeprecatedRouteDeta
if (deprecationType !== 'deprecate') {
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.',
})
i18n.translate(
'core.deprecations.apiRouteDeprecation.manualSteps.routeDepractionMarkAsResolvedStep',
{
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.',
}
)
);
}

View file

@ -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 { buildApiRouteDeprecationDetails, getIsRouteApiDeprecation } from './route_deprecations';

View file

@ -0,0 +1,65 @@
/*
* 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 {
ApiDeprecationDetails,
DomainDeprecationDetails,
} from '@kbn/core-deprecations-common';
import _ from 'lodash';
import type { PostValidationMetadata } from '@kbn/core-http-server';
import {
getApiDeprecationMessage,
getApiDeprecationsManualSteps,
getApiDeprecationTitle,
} from './i18n_texts';
import type { BuildApiDeprecationDetailsParams } from '../types';
export const getIsRouteApiDeprecation = ({
isInternalApiRequest,
deprecated,
}: PostValidationMetadata): boolean => {
const hasDeprecatedObject = deprecated && _.isObject(deprecated);
const isNotInternalRequest = !isInternalApiRequest;
return !!(hasDeprecatedObject && isNotInternalRequest);
};
export const buildApiRouteDeprecationDetails = ({
apiUsageStats,
deprecatedApiDetails,
}: BuildApiDeprecationDetailsParams): DomainDeprecationDetails<ApiDeprecationDetails> => {
const { apiId, apiTotalCalls, totalMarkedAsResolved } = apiUsageStats;
const { routeVersion, routePath, routeDeprecationOptions, routeMethod } = deprecatedApiDetails;
if (!routeDeprecationOptions) {
throw new Error(`Expecing deprecated to be defined for route ${apiId}`);
}
const deprecationLevel = routeDeprecationOptions.severity || 'warning';
return {
apiId,
title: getApiDeprecationTitle(deprecatedApiDetails),
level: deprecationLevel,
message: getApiDeprecationMessage(deprecatedApiDetails, apiUsageStats),
documentationUrl: routeDeprecationOptions.documentationUrl,
correctiveActions: {
manualSteps: getApiDeprecationsManualSteps(deprecatedApiDetails),
mark_as_resolved_api: {
routePath,
routeMethod,
routeVersion,
apiTotalCalls,
totalMarkedAsResolved,
timestamp: new Date(),
},
},
deprecationType: 'api',
domainId: 'core.http.routes-deprecations',
};
};

View file

@ -0,0 +1,25 @@
/*
* 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 { RouterDeprecatedApiDetails } from '@kbn/core-http-server';
import type { CoreDeprecatedApiUsageStats } from '@kbn/core-usage-data-server';
import type { DeprecationsFactory } from '../../deprecations_factory';
export interface ApiDeprecationsServiceDeps {
deprecationsFactory: DeprecationsFactory;
http: InternalHttpServiceSetup;
coreUsageData: InternalCoreUsageDataSetup;
}
export interface BuildApiDeprecationDetailsParams {
apiUsageStats: CoreDeprecatedApiUsageStats;
deprecatedApiDetails: RouterDeprecatedApiDetails;
}

View file

@ -7,5 +7,10 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
export { buildApiDeprecationId, registerApiDeprecationsInfo } from './api_deprecations';
export {
buildApiDeprecationId,
registerApiDeprecationsInfo,
getIsAccessApiDeprecation,
getIsRouteApiDeprecation,
} from './api_deprecations';
export { registerConfigDeprecationsInfo } from './config_deprecations';

View file

@ -52,7 +52,10 @@ describe('DeprecationsService', () => {
expect(http.createRouter).toBeCalledWith('/api/deprecations');
// registers get route '/'
expect(router.get).toHaveBeenCalledTimes(1);
expect(router.get).toHaveBeenCalledWith({ path: '/', validate: false }, expect.any(Function));
expect(router.get).toHaveBeenCalledWith(
{ options: { access: 'public' }, path: '/', validate: false },
expect.any(Function)
);
});
it('calls registerConfigDeprecationsInfo', async () => {

View file

@ -14,6 +14,9 @@ export const registerGetRoute = (router: InternalDeprecationRouter) => {
router.get(
{
path: '/',
options: {
access: 'public',
},
validate: false,
},
async (context, req, res) => {

View file

@ -10,9 +10,9 @@
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'; // shouldn't use deep imports
import type { PostValidationMetadata } from '@kbn/core-http-server';
import { buildApiDeprecationId } from '../deprecations';
import { getIsRouteApiDeprecation, getIsAccessApiDeprecation } from '../deprecations';
interface Dependencies {
coreUsageData: InternalCoreUsageDataSetup;
@ -35,8 +35,11 @@ export function createRouteDeprecationsHandler({
}: {
coreUsageData: InternalCoreUsageDataSetup;
}) {
return (req: CoreKibanaRequest, { deprecated }: { deprecated?: RouteDeprecationInfo }) => {
if (deprecated && isObject(deprecated) && req.route.routePath) {
return (req: CoreKibanaRequest, metadata: PostValidationMetadata) => {
const hasRouteDeprecation = getIsRouteApiDeprecation(metadata);
const hasAccessDeprecation = getIsAccessApiDeprecation(metadata);
const isApiDeprecation = hasAccessDeprecation || hasRouteDeprecation;
if (isApiDeprecation && req.route.routePath) {
const counterName = buildApiDeprecationId({
routeMethod: req.route.method,
routePath: req.route.routePath,

View file

@ -19,6 +19,9 @@ export const registerMarkAsResolvedRoute = (
router.post(
{
path: '/mark_as_resolved',
options: {
access: 'internal',
},
validate: {
body: schema.object({
domainId: schema.string(),

View file

@ -177,9 +177,14 @@ export class CoreKibanaRequest<
this.headers = isRealReq ? deepFreeze({ ...request.headers }) : request.headers;
this.isSystemRequest = this.headers['kbn-system-request'] === 'true';
this.isFakeRequest = !isRealReq;
// set to false if elasticInternalOrigin is explicitly set to false
// otherwise check for the header or the query param
this.isInternalApiRequest =
X_ELASTIC_INTERNAL_ORIGIN_REQUEST in this.headers ||
Boolean(this.url?.searchParams?.has(ELASTIC_INTERNAL_ORIGIN_QUERY_PARAM));
this.url?.searchParams?.get(ELASTIC_INTERNAL_ORIGIN_QUERY_PARAM) === 'false'
? false
: X_ELASTIC_INTERNAL_ORIGIN_REQUEST in this.headers ||
this.url?.searchParams?.has(ELASTIC_INTERNAL_ORIGIN_QUERY_PARAM);
// prevent Symbol exposure via Object.getOwnPropertySymbols()
Object.defineProperty(this, requestSymbol, {
value: request,

View file

@ -28,12 +28,12 @@ import type {
VersionedRouter,
RouteRegistrar,
RouteSecurity,
PostValidationMetadata,
} from '@kbn/core-http-server';
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';
@ -287,10 +287,13 @@ export class Router<Context extends RequestHandlerContextBase = RequestHandlerCo
/** Should be private, just exposed for convenience for the versioned router */
public emitPostValidate = (
request: KibanaRequest,
routeOptions: { deprecated?: RouteDeprecationInfo } = {}
postValidateConext: PostValidationMetadata = {
isInternalApiRequest: true,
isPublicAccess: false,
}
) => {
const postValidate: RouterEvents = 'onPostValidate';
Router.ee.emit(postValidate, request, routeOptions);
Router.ee.emit(postValidate, request, postValidateConext);
};
private async handle<P, Q, B>({
@ -304,7 +307,7 @@ export class Router<Context extends RequestHandlerContextBase = RequestHandlerCo
request: Request;
responseToolkit: ResponseToolkit;
emit?: {
onPostValidation: (req: KibanaRequest, reqOptions: any) => void;
onPostValidation: (req: KibanaRequest, metadata: PostValidationMetadata) => void;
};
isPublicUnversionedRoute: boolean;
handler: RequestHandlerEnhanced<
@ -337,11 +340,19 @@ export class Router<Context extends RequestHandlerContextBase = RequestHandlerCo
// Emit onPostValidation even if validation fails.
const req = CoreKibanaRequest.from(request);
emit?.onPostValidation(req, req.route.options);
emit?.onPostValidation(req, {
deprecated: req.route.options.deprecated,
isInternalApiRequest: req.isInternalApiRequest,
isPublicAccess: req.route.options.access === 'public',
});
return response;
}
emit?.onPostValidation(kibanaRequest, kibanaRequest.route.options);
emit?.onPostValidation(kibanaRequest, {
deprecated: kibanaRequest.route.options.deprecated,
isInternalApiRequest: kibanaRequest.isInternalApiRequest,
isPublicAccess: kibanaRequest.route.options.access === 'public',
});
try {
const kibanaResponse = await handler(kibanaRequest, kibanaResponseFactory);

View file

@ -26,6 +26,7 @@ import type {
RouteSecurity,
RouteMethod,
VersionedRouterRoute,
PostValidationMetadata,
} from '@kbn/core-http-server';
import type { Mutable } from 'utility-types';
import type { HandlerResolutionStrategy, Method, Options } from './types';
@ -210,6 +211,12 @@ export class CoreVersionedRoute implements VersionedRoute {
});
}
const validation = extractValidationSchemaFromHandler(handler);
const postValidateMetadata: PostValidationMetadata = {
deprecated: handler.options.options?.deprecated,
isInternalApiRequest: req.isInternalApiRequest,
isPublicAccess: this.isPublic,
};
if (
validation?.request &&
Boolean(validation.request.body || validation.request.params || validation.request.query)
@ -221,7 +228,8 @@ export class CoreVersionedRoute implements VersionedRoute {
req.query = query;
} catch (e) {
// Emit onPostValidation even if validation fails.
this.router.emitPostValidate(req, handler.options.options);
this.router.emitPostValidate(req, postValidateMetadata);
return res.badRequest({ body: e.message, headers: getVersionHeader(version) });
}
} else {
@ -231,7 +239,7 @@ export class CoreVersionedRoute implements VersionedRoute {
req.query = {};
}
this.router.emitPostValidate(req, handler.options.options);
this.router.emitPostValidate(req, postValidateMetadata);
const response = await handler.fn(ctx, req, res);

View file

@ -35,7 +35,7 @@ import type {
HttpServerInfo,
HttpAuth,
IAuthHeadersStorage,
RouterDeprecatedRouteDetails,
RouterDeprecatedApiDetails,
RouteMethod,
} from '@kbn/core-http-server';
import { performance } from 'perf_hooks';
@ -389,8 +389,8 @@ export class HttpServer {
}
}
private getDeprecatedRoutes(): RouterDeprecatedRouteDetails[] {
const deprecatedRoutes: RouterDeprecatedRouteDetails[] = [];
private getDeprecatedRoutes(): RouterDeprecatedApiDetails[] {
const deprecatedRoutes: RouterDeprecatedApiDetails[] = [];
for (const router of this.registeredRouters) {
const allRouterRoutes = [
@ -404,22 +404,29 @@ export class HttpServer {
...allRouterRoutes
.flat()
.map((route) => {
const access = route.options.access;
if (route.isVersioned === true) {
return [...route.handlers.entries()].map(([_, { options }]) => {
const deprecated = options.options?.deprecated;
return { route, version: `${options.version}`, deprecated };
return { route, version: `${options.version}`, deprecated, access };
});
}
return { route, version: undefined, deprecated: route.options.deprecated };
return { route, version: undefined, deprecated: route.options.deprecated, access };
})
.flat()
.filter(({ deprecated }) => isObject(deprecated))
.flatMap(({ route, deprecated, version }) => {
.filter(({ deprecated, access }) => {
const isRouteDeprecation = isObject(deprecated);
const isAccessDeprecation = access === 'internal';
return isRouteDeprecation || isAccessDeprecation;
})
.flatMap(({ route, deprecated, version, access }) => {
return {
routeDeprecationOptions: deprecated!,
routeMethod: route.method as RouteMethod,
routePath: route.path,
routeVersion: version,
routeAccess: access,
};
})
);

View file

@ -417,6 +417,33 @@ describe('restrictInternal post-auth handler', () => {
const request = createForgeRequest('public', { 'x-elastic-internal-origin': 'Kibana' });
createForwardSuccess(handler, request);
});
it('overrides internal api when elasticInternalOrigin=false is set explicitly', () => {
const handler = createRestrictInternalRoutesPostAuthHandler(
{ ...config, restrictInternalApis: true },
logger
);
// Will be treated as external
const request = createForgeRequest(
'internal',
{ 'x-elastic-internal-origin': 'Kibana' },
{ elasticInternalOrigin: 'false' }
);
responseFactory.badRequest.mockReturnValue('badRequest' as any);
const result = handler(request, responseFactory, toolkit);
expect(toolkit.next).not.toHaveBeenCalled();
expect(responseFactory.badRequest).toHaveBeenCalledTimes(1);
expect(responseFactory.badRequest.mock.calls[0][0]).toMatchInlineSnapshot(`
Object {
"body": "uri [/internal/some-path] with method [get] exists but is not available with the current configuration",
}
`);
expect(result).toEqual('badRequest');
});
});
describe('customHeaders pre-response handler', () => {

View file

@ -16,10 +16,10 @@ import type {
IContextContainer,
HttpServiceSetup,
HttpServiceStart,
RouterDeprecatedRouteDetails,
RouterDeprecatedApiDetails,
} 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 { CoreKibanaRequest } from '@kbn/core-http-router-server-internal';
import type { PostValidationMetadata } from '@kbn/core-http-server';
import type { HttpServerSetup } from './http_server';
import type { ExternalUrlConfig } from './external_url';
import type { InternalStaticAssets } from './static_assets';
@ -58,7 +58,7 @@ export interface InternalHttpServiceSetup
plugin?: PluginOpaqueId
) => IRouter<Context>;
registerOnPostValidation(
cb: (req: CoreKibanaRequest, metadata: { deprecated: RouteDeprecationInfo }) => void
cb: (req: CoreKibanaRequest, metadata: PostValidationMetadata) => void
): void;
registerRouterAfterListening: (router: IRouter) => void;
registerStaticDir: (path: string, dirPath: string) => void;
@ -71,7 +71,7 @@ export interface InternalHttpServiceSetup
contextName: ContextName,
provider: IContextProvider<Context, ContextName>
) => IContextContainer;
getRegisteredDeprecatedApis: () => RouterDeprecatedRouteDetails[];
getRegisteredDeprecatedApis: () => RouterDeprecatedApiDetails[];
}
/** @internal */

View file

@ -93,7 +93,9 @@ export type {
IRouter,
RouteRegistrar,
RouterRoute,
RouterDeprecatedRouteDetails,
RouterDeprecatedApiDetails,
RouterAccessDeprecatedApiDetails,
RouterRouteDeprecatedApiDetails,
IKibanaSocket,
KibanaErrorResponseFactory,
KibanaRedirectionResponseFactory,
@ -121,6 +123,7 @@ export type {
RouteSecurityGetter,
InternalRouteSecurity,
RouteDeprecationInfo,
PostValidationMetadata,
} from './src/router';
export {
validBodyOutput,

View file

@ -12,7 +12,7 @@ import type {
IContextProvider,
IRouter,
RequestHandlerContextBase,
RouterDeprecatedRouteDetails,
RouterDeprecatedApiDetails,
} from './router';
import type {
AuthenticationHandler,
@ -362,12 +362,12 @@ export interface HttpServiceSetup<
getServerInfo: () => HttpServerInfo;
/**
* Provides a list of all registered deprecated routes {{@link RouterDeprecatedRouteDetails | information}}.
* Provides a list of all registered deprecated routes {{@link RouterDeprecatedApiDetails | information}}.
* The routers will be evaluated everytime this function gets called to
* accommodate for any late route registrations
* @returns {RouterDeprecatedRouteDetails[]}
* @returns {RouterDeprecatedApiDetails[]}
*/
getDeprecatedRoutes: () => RouterDeprecatedRouteDetails[];
getDeprecatedRoutes: () => RouterDeprecatedApiDetails[];
}
/** @public */

View file

@ -65,6 +65,7 @@ export type {
Privilege,
PrivilegeSet,
RouteDeprecationInfo,
PostValidationMetadata,
} from './route';
export { validBodyOutput, ReservedPrivilegesSet } from './route';
@ -81,7 +82,14 @@ export type {
LazyValidator,
} from './route_validator';
export { RouteValidationError } from './route_validator';
export type { IRouter, RouteRegistrar, RouterRoute, RouterDeprecatedRouteDetails } from './router';
export type {
IRouter,
RouteRegistrar,
RouterRoute,
RouterDeprecatedApiDetails,
RouterAccessDeprecatedApiDetails,
RouterRouteDeprecatedApiDetails,
} from './router';
export type { IKibanaSocket } from './socket';
export type {
KibanaErrorResponseFactory,

View file

@ -525,3 +525,12 @@ export interface RouteConfig<P, Q, B, Method extends RouteMethod> {
*/
options?: RouteConfigOptions<Method>;
}
/**
* Post Validation Route emitter metadata.
*/
export interface PostValidationMetadata {
deprecated?: RouteDeprecationInfo;
isInternalApiRequest: boolean;
isPublicAccess: boolean;
}

View file

@ -10,7 +10,7 @@
import type { Request, ResponseObject, ResponseToolkit } from '@hapi/hapi';
import type Boom from '@hapi/boom';
import type { VersionedRouter } from '../versioning';
import type { RouteConfig, RouteDeprecationInfo, RouteMethod } from './route';
import type { RouteAccess, RouteConfig, RouteDeprecationInfo, RouteMethod } from './route';
import type { RequestHandler, RequestHandlerWrapper } from './request_handler';
import type { RequestHandlerContextBase } from './request_handler_context';
import type { RouteConfigOptions } from './route';
@ -143,9 +143,22 @@ export interface RouterRoute {
}
/** @public */
export interface RouterDeprecatedRouteDetails {
routeDeprecationOptions: RouteDeprecationInfo;
export interface RouterDeprecatedApiDetails {
routeDeprecationOptions?: RouteDeprecationInfo;
routeMethod: RouteMethod;
routePath: string;
routeVersion?: string;
routeAccess?: RouteAccess;
}
/** @public */
export interface RouterRouteDeprecatedApiDetails extends RouterDeprecatedApiDetails {
routeAccess: 'public';
routeDeprecationOptions: RouteDeprecationInfo;
}
/** @public */
export interface RouterAccessDeprecatedApiDetails extends RouterDeprecatedApiDetails {
routeAccess: 'internal';
routeDeprecationOptions?: RouteDeprecationInfo;
}

View file

@ -145,6 +145,9 @@ export interface CoreUsageStats {
'savedObjectsRepository.resolvedOutcome.conflict'?: number;
'savedObjectsRepository.resolvedOutcome.notFound'?: number;
'savedObjectsRepository.resolvedOutcome.total'?: number;
// API Deprecations counters
'deprecated_api_calls_resolved.total'?: number;
'deprecated_api_calls.total'?: number;
}
/**

View file

@ -1183,6 +1183,18 @@ export function getCoreUsageCollector(
'How many times a saved object has resolved with any of the four possible outcomes.',
},
},
'deprecated_api_calls_resolved.total': {
type: 'integer',
_meta: {
description: 'How many times deprecated APIs has been marked as resolved',
},
},
'deprecated_api_calls.total': {
type: 'integer',
_meta: {
description: 'How many times deprecated APIs has been called.',
},
},
},
fetch() {
return getCoreUsageDataService().getCoreUsageData();

View file

@ -9290,6 +9290,18 @@
"_meta": {
"description": "How many times a saved object has resolved with any of the four possible outcomes."
}
},
"deprecated_api_calls_resolved.total": {
"type": "integer",
"_meta": {
"description": "How many times deprecated APIs has been marked as resolved"
}
},
"deprecated_api_calls.total": {
"type": "integer",
"_meta": {
"description": "How many times deprecated APIs has been called."
}
}
}
},
@ -10887,12 +10899,6 @@
"description": "Non-default value of setting."
}
},
"observability:newLogsOverview": {
"type": "boolean",
"_meta": {
"description": "Enable the new logs overview component."
}
},
"observability:searchExcludedDataTiers": {
"type": "array",
"items": {
@ -10901,6 +10907,12 @@
"description": "Non-default value of setting."
}
}
},
"observability:newLogsOverview": {
"type": "boolean",
"_meta": {
"description": "Enable the new logs overview component."
}
}
}
},

View file

@ -21,14 +21,14 @@ When we want to enable ML model snapshot deprecation warnings again we need to c
There are three sources of deprecation information:
* [**Elasticsearch Deprecation Info API.**](https://www.elastic.co/guide/en/elasticsearch/reference/master/migration-api-deprecation.html)
* [**Elasticsearch Deprecation Info API.**](https://www.elastic.co/guide/en/elasticsearch/reference/main/migration-api-deprecation.html)
This is information about Elasticsearch cluster, node, Machine Learning, and index-level settings that use deprecated features that will be removed or changed in the next major version. ES server engineers are responsible for adding deprecations to the Deprecation Info API.
* [**Elasticsearch deprecation logs.**](https://www.elastic.co/guide/en/elasticsearch/reference/current/logging.html#deprecation-logging)
These surface runtime deprecations, e.g. a Painless script that uses a deprecated accessor or a
request to a deprecated API. These are also generally surfaced as deprecation headers within the
response. Even if the cluster state is good, app maintainers need to watch the logs in case
deprecations are discovered as data is migrated. Starting in 7.x, deprecation logs can be written to a file or a data stream ([#58924](https://github.com/elastic/elasticsearch/pull/58924)). When the data stream exists, the Upgrade Assistant provides a way to analyze the logs through Observability or Discover ([#106521](https://github.com/elastic/kibana/pull/106521)).
* [**Kibana deprecations API.**](https://github.com/elastic/kibana/blob/master/src/core/server/deprecations/README.mdx) This is information about deprecated features and configs in Kibana. These deprecations are only communicated to the user if the deployment is using these features. Kibana engineers are responsible for adding deprecations to the deprecations API for their respective team.
* [**Kibana deprecations API.**](https://github.com/elastic/kibana/blob/main/src/core/server/deprecations/README.mdx) This is information about deprecated features and configs in Kibana. These deprecations are only communicated to the user if the deployment is using these features. Kibana engineers are responsible for adding deprecations to the deprecations API for their respective team.
### Fixing problems
@ -284,23 +284,27 @@ yarn start --plugin-path=examples/routing_example --plugin-path=examples/develop
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:
We need to explicitly set the query param `elasticInternalOrigin` to `false` to track the request as non-internal origin.
```
# Versioned routes: Version 1 is deprecated
GET kbn:/api/routing_example/d/versioned?apiVersion=1
GET kbn:/api/routing_example/d/versioned?apiVersion=2
# Route deprecations for Versioned routes
GET kbn:/api/routing_example/d/versioned_route?apiVersion=2023-10-31&elasticInternalOrigin=false
# Non-versioned routes
GET kbn:/api/routing_example/d/removed_route
GET kbn:/api/routing_example/d/deprecated_route
POST kbn:/api/routing_example/d/migrated_route
# Route deprecations for Non-versioned routes
GET kbn:/api/routing_example/d/removed_route?elasticInternalOrigin=false
GET kbn:/api/routing_example/d/deprecated_route?elasticInternalOrigin=false
POST kbn:/api/routing_example/d/migrated_route?elasticInternalOrigin=false
{}
# Access deprecations
GET kbn:/api/routing_example/d/internal_deprecated_route?elasticInternalOrigin=false
GET kbn:/internal/routing_example/d/internal_only_route?elasticInternalOrigin=false
GET kbn:/internal/routing_example/d/internal_versioned_route?apiVersion=1&elasticInternalOrigin=false
```
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:
3. Internally you can see the deprecations counters from the dev console by running the following:
```
GET .kibana_usage_counters/_search
{
@ -331,7 +335,7 @@ This is a non-exhaustive list of different error scenarios in Upgrade Assistant.
### Telemetry
The Upgrade Assistant tracks several triggered events in the UI, using Kibana Usage Collection service's [UI counters](https://github.com/elastic/kibana/blob/master/src/plugins/usage_collection/README.mdx#ui-counters).
The Upgrade Assistant tracks several triggered events in the UI, using Kibana Usage Collection service's [UI counters](https://github.com/elastic/kibana/blob/main/src/plugins/usage_collection/README.mdx#ui-counters).
**Overview page**
- Component loaded
@ -350,6 +354,6 @@ The Upgrade Assistant tracks several triggered events in the UI, using Kibana Us
- Component loaded
- Click event for "Quick resolve" button
In addition to UI counters, the Upgrade Assistant has a [custom usage collector](https://github.com/elastic/kibana/blob/master/src/plugins/usage_collection/README.mdx#custom-collector). It currently is only responsible for tracking whether the user has deprecation logging enabled or not.
In addition to UI counters, the Upgrade Assistant has a [custom usage collector](https://github.com/elastic/kibana/blob/main/src/plugins/usage_collection/README.mdx#custom-collector). It currently is only responsible for tracking whether the user has deprecation logging enabled or not.
For testing instructions, refer to the [Kibana Usage Collection service README](https://github.com/elastic/kibana/blob/master/src/plugins/usage_collection/README.mdx#testing).
For testing instructions, refer to the [Kibana Usage Collection service README](https://github.com/elastic/kibana/blob/main/src/plugins/usage_collection/README.mdx#testing).

View file

@ -7,21 +7,19 @@
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 type {
ApiDeprecationDetails,
DomainDeprecationDetails,
} from '@kbn/core-deprecations-common';
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[];
) as unknown as Array<DomainDeprecationDetails<ApiDeprecationDetails>>;
};
export default function ({ getService }: FtrProviderContext) {
@ -30,7 +28,10 @@ export default function ({ getService }: FtrProviderContext) {
const retry = getService('retry');
const es = getService('es');
describe('Kibana API Deprecations', () => {
describe('Kibana API Deprecations', function () {
// bail on first error in this suite since cases sequentially depend on each other
this.bail(true);
before(async () => {
// await kibanaServer.savedObjects.cleanStandardList();
await esArchiver.emptyKibanaIndex();
@ -42,6 +43,9 @@ export default function ({ getService }: FtrProviderContext) {
});
it('returns deprecated APIs when the api is called', async () => {
await supertest
.get(`/internal/routing_example/d/internal_versioned_route?apiVersion=1`)
.expect(200);
await supertest.get(`/api/routing_example/d/removed_route`).expect(200);
// sleep a little until the usage counter is synced into ES
@ -51,7 +55,7 @@ export default function ({ getService }: FtrProviderContext) {
async () => {
const { deprecations } = (await supertest.get(`/api/deprecations/`).expect(200)).body;
const apiDeprecations = getApiDeprecations(deprecations);
expect(apiDeprecations.length).to.equal(1);
expect(apiDeprecations.length).to.equal(2);
expectExpect(apiDeprecations[0].correctiveActions.mark_as_resolved_api).toEqual({
routePath: '/api/routing_example/d/removed_route',
@ -68,6 +72,23 @@ export default function ({ getService }: FtrProviderContext) {
expectExpect(apiDeprecations[0].title).toEqual(
'The "GET /api/routing_example/d/removed_route" route is removed'
);
expectExpect(apiDeprecations[1].correctiveActions.mark_as_resolved_api).toEqual({
routePath: '/internal/routing_example/d/internal_versioned_route',
routeMethod: 'get',
routeVersion: '1',
apiTotalCalls: 1,
totalMarkedAsResolved: 0,
timestamp: expectExpect.any(String),
});
expectExpect(apiDeprecations[1].domainId).toEqual('core.api_deprecations');
expectExpect(apiDeprecations[1].apiId).toEqual(
'1|get|/internal/routing_example/d/internal_versioned_route'
);
expectExpect(apiDeprecations[1].title).toEqual(
'The "GET /internal/routing_example/d/internal_versioned_route" API is internal to Elastic'
);
},
undefined,
2000
@ -76,7 +97,7 @@ export default function ({ getService }: FtrProviderContext) {
it('no longer returns deprecated API when it is marked as resolved', async () => {
await supertest
.post(`/api/deprecations/mark_as_resolved`)
.post(`/api/deprecations/mark_as_resolved?elasticInternalOrigin=true`)
.set('kbn-xsrf', 'xxx')
.send({
domainId: 'core.api_deprecations',
@ -91,7 +112,10 @@ export default function ({ getService }: FtrProviderContext) {
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);
expect(apiDeprecations.length).to.equal(1);
expectExpect(apiDeprecations[0].apiId).toEqual(
'1|get|/internal/routing_example/d/internal_versioned_route'
);
});
});
@ -105,7 +129,7 @@ export default function ({ getService }: FtrProviderContext) {
async () => {
const { deprecations } = (await supertest.get(`/api/deprecations/`).expect(200)).body;
const apiDeprecations = getApiDeprecations(deprecations);
expect(apiDeprecations.length).to.equal(1);
expect(apiDeprecations.length).to.equal(2);
expectExpect(apiDeprecations[0].correctiveActions.mark_as_resolved_api).toEqual({
routePath: '/api/routing_example/d/removed_route',
@ -132,31 +156,80 @@ export default function ({ getService }: FtrProviderContext) {
},
});
expect(hits.hits.length).to.equal(3);
expect(hits.hits.length).to.equal(4);
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',
},
]);
expectExpect(_.sortBy(counters, 'counterType')).toEqual(expectedSuiteUsageCounters);
});
it('Does not increment internal origin calls', async () => {
await supertest
.get(`/api/routing_example/d/removed_route?elasticInternalOrigin=true`)
.expect(200);
// call another deprecated api to make sure that we are not verifying stale results
await supertest
.get(`/api/routing_example/d/versioned_route?apiVersion=2023-10-31`)
.expect(200);
// sleep a little until the usage counter is synced into ES
await setTimeoutAsync(3000);
await retry.tryForTime(15 * 1000, 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(5);
const counters = hits.hits.map((hit) => hit._source!['usage-counter']).sort();
expectExpect(_.sortBy(counters, 'counterType')).toEqual(
[
...expectedSuiteUsageCounters,
{
domainId: 'core',
counterName: '2023-10-31|get|/api/routing_example/d/versioned_route',
counterType: 'deprecated_api_call:total',
source: 'server',
count: 1,
},
].sort()
);
});
});
});
}
const expectedSuiteUsageCounters = [
{
domainId: 'core',
counterName: 'unversioned|get|/api/routing_example/d/removed_route',
counterType: 'deprecated_api_call:marked_as_resolved',
source: 'server',
count: 1,
},
{
domainId: 'core',
counterName: 'unversioned|get|/api/routing_example/d/removed_route',
counterType: 'deprecated_api_call:resolved',
source: 'server',
count: 1,
},
{
domainId: 'core',
counterName: '1|get|/internal/routing_example/d/internal_versioned_route',
counterType: 'deprecated_api_call:total',
source: 'server',
count: 1,
},
{
domainId: 'core',
counterName: 'unversioned|get|/api/routing_example/d/removed_route',
counterType: 'deprecated_api_call:total',
source: 'server',
count: 2,
},
];