Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

legacy dashboards import/export API: deprecation logs and usage data #111283

Merged
merged 11 commits into from
Sep 8, 2021
1 change: 0 additions & 1 deletion .github/CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,6 @@
/src/plugins/kibana_overview/ @elastic/kibana-core
/x-pack/plugins/global_search_bar/ @elastic/kibana-core
#CC# /src/core/server/csp/ @elastic/kibana-core
#CC# /src/plugins/legacy_export/ @elastic/kibana-core
#CC# /src/plugins/xpack_legacy/ @elastic/kibana-core
#CC# /src/plugins/saved_objects/ @elastic/kibana-core
#CC# /x-pack/plugins/cloud/ @elastic/kibana-core
Expand Down
4 changes: 0 additions & 4 deletions docs/developer/plugin-list.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -172,10 +172,6 @@ in Kibana, e.g. visualizations. It has the form of a flyout panel.
|Utilities for building Kibana plugins.


|{kib-repo}blob/{branch}/src/plugins/legacy_export/README.md[legacyExport]
|The legacyExport plugin adds support for the legacy saved objects export format.


|{kib-repo}blob/{branch}/src/plugins/management/README.md[management]
|This plugins contains the "Stack Management" page framework. It offers navigation and an API
to link individual managment section into it. This plugin does not contain any individual
Expand Down
8 changes: 0 additions & 8 deletions docs/user/api.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -51,14 +51,6 @@ Calls to the API endpoints require different operations. To interact with the {k

* *DELETE* - Removes the information.

For example, the following `curl` command exports a dashboard:

[source,sh]
--------------------------------------------
curl -X POST api/kibana/dashboards/export?dashboard=942dcef0-b2cd-11e8-ad8e-85441f0c2e5c
--------------------------------------------
// KIBANA

[float]
[[api-request-headers]]
=== Request headers
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ const createUsageStatsClientMock = () =>
incrementSavedObjectsImport: jest.fn().mockResolvedValue(null),
incrementSavedObjectsResolveImportErrors: jest.fn().mockResolvedValue(null),
incrementSavedObjectsExport: jest.fn().mockResolvedValue(null),
incrementLegacyDashboardsImport: jest.fn().mockResolvedValue(null),
incrementLegacyDashboardsExport: jest.fn().mockResolvedValue(null),
} as unknown) as jest.Mocked<CoreUsageStatsClient>);

export const coreUsageStatsClientMock = {
Expand Down
112 changes: 112 additions & 0 deletions src/core/server/core_usage_data/core_usage_stats_client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ import {
IMPORT_STATS_PREFIX,
RESOLVE_IMPORT_STATS_PREFIX,
EXPORT_STATS_PREFIX,
LEGACY_DASHBOARDS_IMPORT_STATS_PREFIX,
LEGACY_DASHBOARDS_EXPORT_STATS_PREFIX,
} from './core_usage_stats_client';
import { CoreUsageStatsClient } from '.';
import { DEFAULT_NAMESPACE_STRING } from '../saved_objects/service/lib/utils';
Expand Down Expand Up @@ -1007,4 +1009,114 @@ describe('CoreUsageStatsClient', () => {
);
});
});

describe('#incrementLegacyDashboardsImport', () => {
it('does not throw an error if repository incrementCounter operation fails', async () => {
const { usageStatsClient, repositoryMock } = setup();
repositoryMock.incrementCounter.mockRejectedValue(new Error('Oh no!'));

const request = httpServerMock.createKibanaRequest();
await expect(
usageStatsClient.incrementLegacyDashboardsImport({
request,
} as IncrementSavedObjectsExportOptions)
).resolves.toBeUndefined();
expect(repositoryMock.incrementCounter).toHaveBeenCalled();
});

it('handles the default namespace string and first party request appropriately', async () => {
const { usageStatsClient, repositoryMock } = setup(DEFAULT_NAMESPACE_STRING);

const request = httpServerMock.createKibanaRequest({ headers: firstPartyRequestHeaders });
await usageStatsClient.incrementLegacyDashboardsImport({
request,
} as IncrementSavedObjectsExportOptions);
expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1);
expect(repositoryMock.incrementCounter).toHaveBeenCalledWith(
CORE_USAGE_STATS_TYPE,
CORE_USAGE_STATS_ID,
[
`${LEGACY_DASHBOARDS_IMPORT_STATS_PREFIX}.total`,
`${LEGACY_DASHBOARDS_IMPORT_STATS_PREFIX}.namespace.default.total`,
`${LEGACY_DASHBOARDS_IMPORT_STATS_PREFIX}.namespace.default.kibanaRequest.yes`,
],
incrementOptions
);
});

it('handles a non-default space and and third party request appropriately', async () => {
const { usageStatsClient, repositoryMock } = setup('foo');

const request = httpServerMock.createKibanaRequest();
await usageStatsClient.incrementLegacyDashboardsImport({
request,
} as IncrementSavedObjectsExportOptions);
expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1);
expect(repositoryMock.incrementCounter).toHaveBeenCalledWith(
CORE_USAGE_STATS_TYPE,
CORE_USAGE_STATS_ID,
[
`${LEGACY_DASHBOARDS_IMPORT_STATS_PREFIX}.total`,
`${LEGACY_DASHBOARDS_IMPORT_STATS_PREFIX}.namespace.custom.total`,
`${LEGACY_DASHBOARDS_IMPORT_STATS_PREFIX}.namespace.custom.kibanaRequest.no`,
],
incrementOptions
);
});
});

describe('#incrementLegacyDashboardsExport', () => {
it('does not throw an error if repository incrementCounter operation fails', async () => {
const { usageStatsClient, repositoryMock } = setup();
repositoryMock.incrementCounter.mockRejectedValue(new Error('Oh no!'));

const request = httpServerMock.createKibanaRequest();
await expect(
usageStatsClient.incrementLegacyDashboardsExport({
request,
} as IncrementSavedObjectsExportOptions)
).resolves.toBeUndefined();
expect(repositoryMock.incrementCounter).toHaveBeenCalled();
});

it('handles the default namespace string and first party request appropriately', async () => {
const { usageStatsClient, repositoryMock } = setup(DEFAULT_NAMESPACE_STRING);

const request = httpServerMock.createKibanaRequest({ headers: firstPartyRequestHeaders });
await usageStatsClient.incrementLegacyDashboardsExport({
request,
} as IncrementSavedObjectsExportOptions);
expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1);
expect(repositoryMock.incrementCounter).toHaveBeenCalledWith(
CORE_USAGE_STATS_TYPE,
CORE_USAGE_STATS_ID,
[
`${LEGACY_DASHBOARDS_EXPORT_STATS_PREFIX}.total`,
`${LEGACY_DASHBOARDS_EXPORT_STATS_PREFIX}.namespace.default.total`,
`${LEGACY_DASHBOARDS_EXPORT_STATS_PREFIX}.namespace.default.kibanaRequest.yes`,
],
incrementOptions
);
});

it('handles a non-default space and and third party request appropriately', async () => {
const { usageStatsClient, repositoryMock } = setup('foo');

const request = httpServerMock.createKibanaRequest();
await usageStatsClient.incrementLegacyDashboardsExport({
request,
} as IncrementSavedObjectsExportOptions);
expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1);
expect(repositoryMock.incrementCounter).toHaveBeenCalledWith(
CORE_USAGE_STATS_TYPE,
CORE_USAGE_STATS_ID,
[
`${LEGACY_DASHBOARDS_EXPORT_STATS_PREFIX}.total`,
`${LEGACY_DASHBOARDS_EXPORT_STATS_PREFIX}.namespace.custom.total`,
`${LEGACY_DASHBOARDS_EXPORT_STATS_PREFIX}.namespace.custom.kibanaRequest.no`,
],
incrementOptions
);
});
});
});
11 changes: 11 additions & 0 deletions src/core/server/core_usage_data/core_usage_stats_client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ export const UPDATE_STATS_PREFIX = 'apiCalls.savedObjectsUpdate';
export const IMPORT_STATS_PREFIX = 'apiCalls.savedObjectsImport';
export const RESOLVE_IMPORT_STATS_PREFIX = 'apiCalls.savedObjectsResolveImportErrors';
export const EXPORT_STATS_PREFIX = 'apiCalls.savedObjectsExport';
export const LEGACY_DASHBOARDS_IMPORT_STATS_PREFIX = 'apiCalls.legacyDashboardImport';
export const LEGACY_DASHBOARDS_EXPORT_STATS_PREFIX = 'apiCalls.legacyDashboardExport';
Comment on lines +48 to +49
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should these be added to ALL_COUNTER_FIELDS?


export const REPOSITORY_RESOLVE_OUTCOME_STATS = {
EXACT_MATCH: 'savedObjectsRepository.resolvedOutcome.exactMatch',
ALIAS_MATCH: 'savedObjectsRepository.resolvedOutcome.aliasMatch',
Expand Down Expand Up @@ -170,6 +173,14 @@ export class CoreUsageStatsClient {
await this.updateUsageStats(counterFieldNames, EXPORT_STATS_PREFIX, options);
}

public async incrementLegacyDashboardsImport(options: BaseIncrementOptions) {
await this.updateUsageStats([], LEGACY_DASHBOARDS_IMPORT_STATS_PREFIX, options);
}

public async incrementLegacyDashboardsExport(options: BaseIncrementOptions) {
await this.updateUsageStats([], LEGACY_DASHBOARDS_EXPORT_STATS_PREFIX, options);
}

private async updateUsageStats(
counterFieldNames: string[],
prefix: string,
Expand Down
12 changes: 12 additions & 0 deletions src/core/server/saved_objects/routes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,19 +25,23 @@ import { registerExportRoute } from './export';
import { registerImportRoute } from './import';
import { registerResolveImportErrorsRoute } from './resolve_import_errors';
import { registerMigrateRoute } from './migrate';
import { registerLegacyImportRoute } from './legacy_import_export/import';
import { registerLegacyExportRoute } from './legacy_import_export/export';

export function registerRoutes({
http,
coreUsageData,
logger,
config,
migratorPromise,
kibanaVersion,
}: {
http: InternalHttpServiceSetup;
coreUsageData: InternalCoreUsageDataSetup;
logger: Logger;
config: SavedObjectConfig;
migratorPromise: Promise<IKibanaMigrator>;
kibanaVersion: string;
}) {
const router = http.createRouter('/api/saved_objects/');

Expand All @@ -55,6 +59,14 @@ export function registerRoutes({
registerImportRoute(router, { config, coreUsageData });
registerResolveImportErrorsRoute(router, { config, coreUsageData });

const legacyRouter = http.createRouter('');
registerLegacyImportRoute(legacyRouter, {
maxImportPayloadBytes: config.maxImportPayloadBytes,
coreUsageData,
logger,
});
registerLegacyExportRoute(legacyRouter, { kibanaVersion, coreUsageData, logger });

const internalRouter = http.createRouter('/internal/saved_objects/');

registerMigrateRoute(internalRouter, migratorPromise);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,18 @@

import moment from 'moment';
import { schema } from '@kbn/config-schema';
import { IRouter } from 'src/core/server';
import { exportDashboards } from '../lib';
import { InternalCoreUsageDataSetup } from 'src/core/server/core_usage_data';
import { IRouter, Logger } from '../../..';
import { exportDashboards } from './lib';

export const registerExportRoute = (router: IRouter, kibanaVersion: string) => {
export const registerLegacyExportRoute = (
router: IRouter,
{
kibanaVersion,
coreUsageData,
logger,
}: { kibanaVersion: string; coreUsageData: InternalCoreUsageDataSetup; logger: Logger }
) => {
router.get(
{
path: '/api/kibana/dashboards/export',
Expand All @@ -25,9 +33,16 @@ export const registerExportRoute = (router: IRouter, kibanaVersion: string) => {
},
},
async (ctx, req, res) => {
logger.warn(
"The export dashboard API '/api/kibana/dashboards/export' is deprecated. Use the saved objects import objects API '/api/saved_objects/_export' instead."
);

const ids = Array.isArray(req.query.dashboard) ? req.query.dashboard : [req.query.dashboard];
const { client } = ctx.core.savedObjects;

const usageStatsClient = coreUsageData.getClient();
usageStatsClient.incrementLegacyDashboardsExport({ request: req }).catch(() => {});

const exported = await exportDashboards(ids, client, kibanaVersion);
const filename = `kibana-dashboards.${moment.utc().format('YYYY-MM-DD-HH-mm-ss')}.json`;
const body = JSON.stringify(exported, null, ' ');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,18 @@
*/

import { schema } from '@kbn/config-schema';
import { IRouter, SavedObject } from 'src/core/server';
import { importDashboards } from '../lib';
import { IRouter, Logger, SavedObject } from '../../..';
import { InternalCoreUsageDataSetup } from '../../../core_usage_data';
import { importDashboards } from './lib';

export const registerImportRoute = (router: IRouter, maxImportPayloadBytes: number) => {
export const registerLegacyImportRoute = (
router: IRouter,
{
maxImportPayloadBytes,
coreUsageData,
logger,
}: { maxImportPayloadBytes: number; coreUsageData: InternalCoreUsageDataSetup; logger: Logger }
) => {
router.post(
{
path: '/api/kibana/dashboards/import',
Expand All @@ -34,9 +42,17 @@ export const registerImportRoute = (router: IRouter, maxImportPayloadBytes: numb
},
},
async (ctx, req, res) => {
logger.warn(
"The import dashboard API '/api/kibana/dashboards/import' is deprecated. Use the saved objects import objects API '/api/saved_objects/_import' instead."
);

const { client } = ctx.core.savedObjects;
const objects = req.body.objects as SavedObject[];
const { force, exclude } = req.query;

const usageStatsClient = coreUsageData.getClient();
usageStatsClient.incrementLegacyDashboardsImport({ request: req }).catch(() => {});

const result = await importDashboards(client, objects, {
overwrite: force,
exclude: Array.isArray(exclude) ? exclude : [exclude],
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/*
* 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 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 or the Server
* Side Public License, v 1.
*/

const exportObjects = [
{
id: '1',
type: 'index-pattern',
attributes: {},
references: [],
},
{
id: '2',
type: 'search',
attributes: {},
references: [
{
name: 'ref_0',
type: 'index-pattern',
id: '1',
},
],
},
];

jest.mock('../lib/export_dashboards', () => ({
exportDashboards: jest.fn().mockResolvedValue({ version: 'mockversion', objects: exportObjects }),
}));

import supertest from 'supertest';
import type { UnwrapPromise } from '@kbn/utility-types';
import { CoreUsageStatsClient } from '../../../../core_usage_data';
import { coreUsageStatsClientMock } from '../../../../core_usage_data/core_usage_stats_client.mock';
import { coreUsageDataServiceMock } from '../../../../core_usage_data/core_usage_data_service.mock';
import { registerLegacyExportRoute } from '../export';
import { setupServer } from '../../test_utils';
import { loggerMock } from 'src/core/server/logging/logger.mock';

type SetupServerReturn = UnwrapPromise<ReturnType<typeof setupServer>>;
let coreUsageStatsClient: jest.Mocked<CoreUsageStatsClient>;

describe('POST /api/dashboards/export', () => {
let server: SetupServerReturn['server'];
let httpSetup: SetupServerReturn['httpSetup'];

beforeEach(async () => {
({ server, httpSetup } = await setupServer());

const router = httpSetup.createRouter('');

coreUsageStatsClient = coreUsageStatsClientMock.create();
coreUsageStatsClient.incrementLegacyDashboardsExport.mockRejectedValue(new Error('Oh no!')); // intentionally throw this error, which is swallowed, so we can assert that the operation does not fail
const coreUsageData = coreUsageDataServiceMock.createSetupContract(coreUsageStatsClient);
registerLegacyExportRoute(router, {
kibanaVersion: '7.14.0',
coreUsageData,
logger: loggerMock.create(),
});

await server.start();
});

afterEach(async () => {
jest.clearAllMocks();
await server.stop();
});

it('calls exportDashboards and records usage stats', async () => {
const result = await supertest(httpSetup.server.listener).get(
'/api/kibana/dashboards/export?dashboard=942dcef0-b2cd-11e8-ad8e-85441f0c2e5c'
);

expect(result.status).toBe(200);
expect(result.header['content-type']).toEqual('application/json; charset=utf-8');
expect(result.header['content-disposition']).toMatch(
/attachment; filename="kibana-dashboards.*\.json/
);

expect(result.body.objects).toEqual(exportObjects);
expect(result.body.version).toEqual('mockversion');
expect(coreUsageStatsClient.incrementLegacyDashboardsExport).toHaveBeenCalledWith({
request: expect.anything(),
});
});
});
Loading