Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 };
Copy link
Contributor

Choose a reason for hiding this comment

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

I love this! It makes it so much easier to track what the test server is doing 😄

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