Skip to content

Commit

Permalink
[FTR] KbnClientSavedObjects improvements (elastic#149582)
Browse files Browse the repository at this point in the history
## Summary

Follow-up of elastic#149188


- Use the bulkDelete API for `KbnClientSavedObjects.bulkDelete`
- Create a dedicated `/_clean` endpoint for
`KbnClientSavedObjects.clean` and
`KbnClientSavedObjects.cleanStandardList`

---------

Co-authored-by: kibanamachine <[email protected]>
  • Loading branch information
2 people authored and kqualters-elastic committed Feb 6, 2023
1 parent 82ef3e1 commit 98e8a41
Show file tree
Hide file tree
Showing 8 changed files with 231 additions and 111 deletions.
14 changes: 12 additions & 2 deletions packages/kbn-test/src/kbn_client/import_export/parse_archive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,19 @@ export interface SavedObject {
[key: string]: unknown;
}

export async function parseArchive(path: string): Promise<SavedObject[]> {
export async function parseArchive(
path: string,
{ stripSummary = false }: { stripSummary?: boolean } = {}
): Promise<SavedObject[]> {
return (await Fs.readFile(path, 'utf-8'))
.split(/\r?\n\r?\n/)
.filter((line) => !!line)
.map((line) => JSON.parse(line));
.map((line) => JSON.parse(line))
.filter(
stripSummary
? (object) => {
return object.type && object.id;
}
: () => true
);
}
3 changes: 2 additions & 1 deletion packages/kbn-test/src/kbn_client/kbn_client_import_export.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ interface ImportApiResponse {
success: boolean;
[key: string]: unknown;
}

export class KbnClientImportExport {
constructor(
public readonly log: ToolingLog,
Expand Down Expand Up @@ -92,7 +93,7 @@ export class KbnClientImportExport {
const src = this.resolveAndValidatePath(path);
this.log.debug('unloading docs from archive at', src);

const objects = await parseArchive(src);
const objects = await parseArchive(src, { stripSummary: true });
this.log.info('deleting', objects.length, 'objects', { space: options?.space });

const { deleted, missing } = await this.savedObjects.bulkDelete({
Expand Down
181 changes: 73 additions & 108 deletions packages/kbn-test/src/kbn_client/kbn_client_saved_objects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,9 @@
* Side Public License, v 1.
*/

import { inspect } from 'util';
import * as Rx from 'rxjs';
import { mergeMap } from 'rxjs/operators';
import { isAxiosResponseError } from '@kbn/dev-utils';
import { createFailError } from '@kbn/dev-cli-errors';
import { ToolingLog } from '@kbn/tooling-log';
import { chunk } from 'lodash';
import type { ToolingLog } from '@kbn/tooling-log';
import type { SavedObjectsBulkDeleteResponse } from '@kbn/core-saved-objects-api-server';

import { KbnClientRequester, uriencode } from './kbn_client_requester';

Expand Down Expand Up @@ -57,22 +54,15 @@ interface MigrateResponse {
result: Array<{ status: string }>;
}

interface FindApiResponse {
saved_objects: Array<{
type: string;
id: string;
[key: string]: unknown;
}>;
total: number;
per_page: number;
page: number;
}

interface CleanOptions {
space?: string;
types: string[];
}

interface CleanApiResponse {
deleted: number;
}

interface DeleteObjectsOptions {
space?: string;
objects: Array<{
Expand All @@ -81,13 +71,43 @@ interface DeleteObjectsOptions {
}>;
}

async function concurrently<T>(maxConcurrency: number, arr: T[], fn: (item: T) => Promise<void>) {
if (arr.length) {
await Rx.lastValueFrom(
Rx.from(arr).pipe(mergeMap(async (item) => await fn(item), maxConcurrency))
);
}
}
const DELETE_CHUNK_SIZE = 50;

// add types here
const STANDARD_LIST_TYPES = [
'url',
'index-pattern',
'action',
'query',
'alert',
'graph-workspace',
'tag',
'visualization',
'canvas-element',
'canvas-workpad',
'dashboard',
'search',
'lens',
'map',
'cases',
'uptime-dynamic-settings',
'osquery-saved-query',
'osquery-pack',
'infrastructure-ui-source',
'metrics-explorer-view',
'inventory-view',
'infrastructure-monitoring-log-view',
'apm-indices',
// Fleet saved object types
'ingest-outputs',
'ingest-download-sources',
'ingest-agent-policies',
'ingest-package-policies',
'epm-packages',
'epm-packages-assets',
'fleet-preconfiguration-deletion-record',
'fleet-fleet-server-host',
];

/**
* SO client for FTR.
Expand Down Expand Up @@ -194,103 +214,48 @@ export class KbnClientSavedObjects {
public async clean(options: CleanOptions) {
this.log.debug('Cleaning all saved objects', { space: options.space });

let deleted = 0;

while (true) {
const resp = await this.requester.request<FindApiResponse>({
method: 'GET',
path: options.space
? uriencode`/s/${options.space}/internal/ftr/kbn_client_so/_find`
: `/internal/ftr/kbn_client_so/_find`,
query: {
per_page: 1000,
type: options.types,
fields: 'none',
},
});

this.log.info('deleting batch of', resp.data.saved_objects.length, 'objects');
const deletion = await this.bulkDelete({
space: options.space,
objects: resp.data.saved_objects,
});
deleted += deletion.deleted;

if (resp.data.total <= resp.data.per_page) {
break;
}
}
const resp = await this.requester.request<CleanApiResponse>({
method: 'POST',
path: options.space
? uriencode`/s/${options.space}/internal/ftr/kbn_client_so/_clean`
: `/internal/ftr/kbn_client_so/_clean`,
body: {
types: options.types,
},
});
const deleted = resp.data.deleted;

this.log.success('deleted', deleted, 'objects');
}

public async cleanStandardList(options?: { space?: string }) {
// add types here
const types = [
'url',
'index-pattern',
'action',
'query',
'alert',
'graph-workspace',
'tag',
'visualization',
'canvas-element',
'canvas-workpad',
'dashboard',
'search',
'lens',
'map',
'cases',
'uptime-dynamic-settings',
'osquery-saved-query',
'osquery-pack',
'infrastructure-ui-source',
'metrics-explorer-view',
'inventory-view',
'infrastructure-monitoring-log-view',
'apm-indices',
// Fleet saved object types
'ingest-outputs',
'ingest-download-sources',
'ingest-agent-policies',
'ingest-package-policies',
'epm-packages',
'epm-packages-assets',
'fleet-preconfiguration-deletion-record',
'fleet-fleet-server-host',
];

const newOptions = { types, space: options?.space };
const newOptions = { types: STANDARD_LIST_TYPES, space: options?.space };
await this.clean(newOptions);
}

public async bulkDelete(options: DeleteObjectsOptions) {
let deleted = 0;
let missing = 0;

await concurrently(20, options.objects, async (obj) => {
try {
await this.requester.request({
method: 'DELETE',
path: options.space
? uriencode`/s/${options.space}/internal/ftr/kbn_client_so/${obj.type}/${obj.id}`
: uriencode`/internal/ftr/kbn_client_so/${obj.type}/${obj.id}`,
});
deleted++;
} catch (error) {
if (isAxiosResponseError(error)) {
if (error.response.status === 404) {
missing++;
return;
}

throw createFailError(`${error.response.status} resp: ${inspect(error.response.data)}`);
}
const chunks = chunk(options.objects, DELETE_CHUNK_SIZE);

throw error;
}
});
for (let i = 0; i < chunks.length; i++) {
const objects = chunks[i];
const { data: response } = await this.requester.request<SavedObjectsBulkDeleteResponse>({
method: 'POST',
path: options.space
? uriencode`/s/${options.space}/internal/ftr/kbn_client_so/_bulk_delete`
: uriencode`/internal/ftr/kbn_client_so/_bulk_delete`,
body: objects.map(({ type, id }) => ({ type, id })),
});
response.statuses.forEach((status) => {
if (status.success) {
deleted++;
} else if (status.error?.statusCode === 404) {
missing++;
}
});
}

return { deleted, missing };
}
Expand Down
1 change: 1 addition & 0 deletions packages/kbn-test/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,6 @@
"@kbn/stdio-dev-helpers",
"@kbn/babel-register",
"@kbn/repo-packages",
"@kbn/core-saved-objects-api-server",
]
}
48 changes: 48 additions & 0 deletions src/plugins/ftr_apis/server/routes/kbn_client_so/clean.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
* 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.
*/

import type { IRouter } from '@kbn/core/server';
import { schema } from '@kbn/config-schema';
import { KBN_CLIENT_API_PREFIX, listHiddenTypes, catchAndReturnBoomErrors } from './utils';

export const registerCleanRoute = (router: IRouter) => {
router.post(
{
path: `${KBN_CLIENT_API_PREFIX}/_clean`,
options: {
tags: ['access:ftrApis'],
},
validate: {
body: schema.object({
types: schema.arrayOf(schema.string()),
}),
},
},
catchAndReturnBoomErrors(async (ctx, req, res) => {
const { types } = req.body;
const { savedObjects } = await ctx.core;
const hiddenTypes = listHiddenTypes(savedObjects.typeRegistry);
const soClient = savedObjects.getClient({ includedHiddenTypes: hiddenTypes });

const finder = soClient.createPointInTimeFinder({ type: types, perPage: 100 });
let deleted = 0;

for await (const response of finder.find()) {
const objects = response.saved_objects.map(({ type, id }) => ({ type, id }));
const { statuses } = await soClient.bulkDelete(objects, { force: true });
deleted += statuses.filter((status) => status.success).length;
}

return res.ok({
body: {
deleted,
},
});
})
);
};
2 changes: 2 additions & 0 deletions src/plugins/ftr_apis/server/routes/kbn_client_so/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { registerDeleteRoute } from './delete';
import { registerFindRoute } from './find';
import { registerGetRoute } from './get';
import { registerUpdateRoute } from './update';
import { registerCleanRoute } from './clean';

export const registerKbnClientSoRoutes = (router: IRouter) => {
registerBulkDeleteRoute(router);
Expand All @@ -21,4 +22,5 @@ export const registerKbnClientSoRoutes = (router: IRouter) => {
registerFindRoute(router);
registerGetRoute(router);
registerUpdateRoute(router);
registerCleanRoute(router);
};
Loading

0 comments on commit 98e8a41

Please sign in to comment.