diff --git a/docs/development/core/server/kibana-plugin-core-server.md b/docs/development/core/server/kibana-plugin-core-server.md index 147a72016b23..a45bd3d44b28 100644 --- a/docs/development/core/server/kibana-plugin-core-server.md +++ b/docs/development/core/server/kibana-plugin-core-server.md @@ -157,6 +157,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [SavedObjectsExportResultDetails](./kibana-plugin-core-server.savedobjectsexportresultdetails.md) | Structure of the export result details entry | | [SavedObjectsFindOptions](./kibana-plugin-core-server.savedobjectsfindoptions.md) | | | [SavedObjectsFindResponse](./kibana-plugin-core-server.savedobjectsfindresponse.md) | Return type of the Saved Objects find() method.\*Note\*: this type is different between the Public and Server Saved Objects clients. | +| [SavedObjectsFindResult](./kibana-plugin-core-server.savedobjectsfindresult.md) | | | [SavedObjectsImportConflictError](./kibana-plugin-core-server.savedobjectsimportconflicterror.md) | Represents a failure to import due to a conflict. | | [SavedObjectsImportError](./kibana-plugin-core-server.savedobjectsimporterror.md) | Represents a failure to import. | | [SavedObjectsImportMissingReferencesError](./kibana-plugin-core-server.savedobjectsimportmissingreferenceserror.md) | Represents a failure to import due to missing references. | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindresponse.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindresponse.md index a1b1a7a05620..4ed069d1598f 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindresponse.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindresponse.md @@ -20,6 +20,6 @@ export interface SavedObjectsFindResponse | --- | --- | --- | | [page](./kibana-plugin-core-server.savedobjectsfindresponse.page.md) | number | | | [per\_page](./kibana-plugin-core-server.savedobjectsfindresponse.per_page.md) | number | | -| [saved\_objects](./kibana-plugin-core-server.savedobjectsfindresponse.saved_objects.md) | Array<SavedObject<T>> | | +| [saved\_objects](./kibana-plugin-core-server.savedobjectsfindresponse.saved_objects.md) | Array<SavedObjectsFindResult<T>> | | | [total](./kibana-plugin-core-server.savedobjectsfindresponse.total.md) | number | | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindresponse.saved_objects.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindresponse.saved_objects.md index adad0dd2b117..7a91367f6ef0 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindresponse.saved_objects.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindresponse.saved_objects.md @@ -7,5 +7,5 @@ Signature: ```typescript -saved_objects: Array>; +saved_objects: Array>; ``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindresult.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindresult.md new file mode 100644 index 000000000000..e455074a7d11 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindresult.md @@ -0,0 +1,19 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsFindResult](./kibana-plugin-core-server.savedobjectsfindresult.md) + +## SavedObjectsFindResult interface + + +Signature: + +```typescript +export interface SavedObjectsFindResult extends SavedObject +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [score](./kibana-plugin-core-server.savedobjectsfindresult.score.md) | number | The Elasticsearch _score of this result. | + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindresult.score.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindresult.score.md new file mode 100644 index 000000000000..c6646df6ee47 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindresult.score.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsFindResult](./kibana-plugin-core-server.savedobjectsfindresult.md) > [score](./kibana-plugin-core-server.savedobjectsfindresult.score.md) + +## SavedObjectsFindResult.score property + +The Elasticsearch `_score` of this result. + +Signature: + +```typescript +score: number; +``` diff --git a/packages/kbn-dev-utils/src/kbn_client/kbn_client.ts b/packages/kbn-dev-utils/src/kbn_client/kbn_client.ts index 2eb6c6cc5aac..861ea0988692 100644 --- a/packages/kbn-dev-utils/src/kbn_client/kbn_client.ts +++ b/packages/kbn-dev-utils/src/kbn_client/kbn_client.ts @@ -18,7 +18,7 @@ */ import { ToolingLog } from '../tooling_log'; -import { KbnClientRequester, ReqOptions } from './kbn_client_requester'; +import { KibanaConfig, KbnClientRequester, ReqOptions } from './kbn_client_requester'; import { KbnClientStatus } from './kbn_client_status'; import { KbnClientPlugins } from './kbn_client_plugins'; import { KbnClientVersion } from './kbn_client_version'; @@ -26,7 +26,7 @@ import { KbnClientSavedObjects } from './kbn_client_saved_objects'; import { KbnClientUiSettings, UiSettingValues } from './kbn_client_ui_settings'; export class KbnClient { - private readonly requester = new KbnClientRequester(this.log, this.kibanaUrls); + private readonly requester = new KbnClientRequester(this.log, this.kibanaConfig); readonly status = new KbnClientStatus(this.requester); readonly plugins = new KbnClientPlugins(this.status); readonly version = new KbnClientVersion(this.status); @@ -43,10 +43,10 @@ export class KbnClient { */ constructor( private readonly log: ToolingLog, - private readonly kibanaUrls: string[], + private readonly kibanaConfig: KibanaConfig, private readonly uiSettingDefaults?: UiSettingValues ) { - if (!kibanaUrls.length) { + if (!kibanaConfig.url) { throw new Error('missing Kibana urls'); } } diff --git a/packages/kbn-dev-utils/src/kbn_client/kbn_client_requester.ts b/packages/kbn-dev-utils/src/kbn_client/kbn_client_requester.ts index ea4159de5574..2aba2be56f27 100644 --- a/packages/kbn-dev-utils/src/kbn_client/kbn_client_requester.ts +++ b/packages/kbn-dev-utils/src/kbn_client/kbn_client_requester.ts @@ -16,10 +16,9 @@ * specific language governing permissions and limitations * under the License. */ - import Url from 'url'; - -import Axios from 'axios'; +import Https from 'https'; +import Axios, { AxiosResponse } from 'axios'; import { isAxiosRequestError, isAxiosResponseError } from '../axios'; import { ToolingLog } from '../tooling_log'; @@ -70,20 +69,38 @@ const delay = (ms: number) => setTimeout(resolve, ms); }); +export interface KibanaConfig { + url: string; + ssl?: { + enabled: boolean; + key: string; + certificate: string; + certificateAuthorities: string; + }; +} + export class KbnClientRequester { - constructor(private readonly log: ToolingLog, private readonly kibanaUrls: string[]) {} + private readonly httpsAgent: Https.Agent | null; + constructor(private readonly log: ToolingLog, private readonly kibanaConfig: KibanaConfig) { + this.httpsAgent = + kibanaConfig.ssl && kibanaConfig.ssl.enabled + ? new Https.Agent({ + cert: kibanaConfig.ssl.certificate, + key: kibanaConfig.ssl.key, + ca: kibanaConfig.ssl.certificateAuthorities, + }) + : null; + } private pickUrl() { - const url = this.kibanaUrls.shift()!; - this.kibanaUrls.push(url); - return url; + return this.kibanaConfig.url; } public resolveUrl(relativeUrl: string = '/') { return Url.resolve(this.pickUrl(), relativeUrl); } - async request(options: ReqOptions): Promise { + async request(options: ReqOptions): Promise> { const url = Url.resolve(this.pickUrl(), options.path); const description = options.description || `${options.method} ${url}`; let attempt = 0; @@ -93,7 +110,7 @@ export class KbnClientRequester { attempt += 1; try { - const response = await Axios.request({ + const response = await Axios.request({ method: options.method, url, data: options.body, @@ -101,9 +118,10 @@ export class KbnClientRequester { headers: { 'kbn-xsrf': 'kbn-client', }, + httpsAgent: this.httpsAgent, }); - return response.data; + return response; } catch (error) { const conflictOnGet = isConcliftOnGetError(error); const requestedRetries = options.retries !== undefined; diff --git a/packages/kbn-dev-utils/src/kbn_client/kbn_client_saved_objects.ts b/packages/kbn-dev-utils/src/kbn_client/kbn_client_saved_objects.ts index e671061b3435..7334c6353deb 100644 --- a/packages/kbn-dev-utils/src/kbn_client/kbn_client_saved_objects.ts +++ b/packages/kbn-dev-utils/src/kbn_client/kbn_client_saved_objects.ts @@ -71,12 +71,13 @@ export class KbnClientSavedObjects { public async migrate() { this.log.debug('Migrating saved objects'); - return await this.requester.request({ + const { data } = await this.requester.request({ description: 'migrate saved objects', path: uriencode`/internal/saved_objects/_migrate`, method: 'POST', body: {}, }); + return data; } /** @@ -85,11 +86,12 @@ export class KbnClientSavedObjects { public async get>(options: GetOptions) { this.log.debug('Gettings saved object: %j', options); - return await this.requester.request>({ + const { data } = await this.requester.request>({ description: 'get saved object', path: uriencode`/api/saved_objects/${options.type}/${options.id}`, method: 'GET', }); + return data; } /** @@ -98,7 +100,7 @@ export class KbnClientSavedObjects { public async create>(options: IndexOptions) { this.log.debug('Creating saved object: %j', options); - return await this.requester.request>({ + const { data } = await this.requester.request>({ description: 'update saved object', path: options.id ? uriencode`/api/saved_objects/${options.type}/${options.id}` @@ -113,6 +115,7 @@ export class KbnClientSavedObjects { references: options.references, }, }); + return data; } /** @@ -121,7 +124,7 @@ export class KbnClientSavedObjects { public async update>(options: UpdateOptions) { this.log.debug('Updating saved object: %j', options); - return await this.requester.request>({ + const { data } = await this.requester.request>({ description: 'update saved object', path: uriencode`/api/saved_objects/${options.type}/${options.id}`, query: { @@ -134,6 +137,7 @@ export class KbnClientSavedObjects { references: options.references, }, }); + return data; } /** @@ -142,10 +146,12 @@ export class KbnClientSavedObjects { public async delete(options: GetOptions) { this.log.debug('Deleting saved object %s/%s', options); - return await this.requester.request({ + const { data } = await this.requester.request({ description: 'delete saved object', path: uriencode`/api/saved_objects/${options.type}/${options.id}`, method: 'DELETE', }); + + return data; } } diff --git a/packages/kbn-dev-utils/src/kbn_client/kbn_client_status.ts b/packages/kbn-dev-utils/src/kbn_client/kbn_client_status.ts index 22baf4a33041..4f203e73620f 100644 --- a/packages/kbn-dev-utils/src/kbn_client/kbn_client_status.ts +++ b/packages/kbn-dev-utils/src/kbn_client/kbn_client_status.ts @@ -52,10 +52,11 @@ export class KbnClientStatus { * Get the full server status */ async get() { - return await this.requester.request({ + const { data } = await this.requester.request({ method: 'GET', path: 'api/status', }); + return data; } /** diff --git a/packages/kbn-dev-utils/src/kbn_client/kbn_client_ui_settings.ts b/packages/kbn-dev-utils/src/kbn_client/kbn_client_ui_settings.ts index dbfa87e70032..6ee2d3bfe59b 100644 --- a/packages/kbn-dev-utils/src/kbn_client/kbn_client_ui_settings.ts +++ b/packages/kbn-dev-utils/src/kbn_client/kbn_client_ui_settings.ts @@ -57,10 +57,11 @@ export class KbnClientUiSettings { * Unset a uiSetting */ async unset(setting: string) { - return await this.requester.request({ + const { data } = await this.requester.request({ path: uriencode`/api/kibana/settings/${setting}`, method: 'DELETE', }); + return data; } /** @@ -105,11 +106,11 @@ export class KbnClientUiSettings { } private async getAll() { - const resp = await this.requester.request({ + const { data } = await this.requester.request({ path: '/api/kibana/settings', method: 'GET', }); - return resp.settings; + return data.settings; } } diff --git a/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts b/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts index 29ec28175a85..e9aeee87f1a3 100644 --- a/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts +++ b/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts @@ -38,6 +38,14 @@ const urlPartsSchema = () => password: Joi.string(), pathname: Joi.string().regex(/^\//, 'start with a /'), hash: Joi.string().regex(/^\//, 'start with a /'), + ssl: Joi.object() + .keys({ + enabled: Joi.boolean().default(false), + certificate: Joi.string().optional(), + certificateAuthorities: Joi.string().optional(), + key: Joi.string().optional(), + }) + .default(), }) .default(); @@ -122,6 +130,7 @@ export const schema = Joi.object() type: Joi.string().valid('chrome', 'firefox', 'ie', 'msedge').default('chrome'), logPollingMs: Joi.number().default(100), + acceptInsecureCerts: Joi.boolean().default(false), }) .default(), diff --git a/packages/kbn-test/src/kbn/index.js b/packages/kbn-test/src/kbn/index.ts similarity index 100% rename from packages/kbn-test/src/kbn/index.js rename to packages/kbn-test/src/kbn/index.ts diff --git a/packages/kbn-test/src/kbn/kbn_test_config.js b/packages/kbn-test/src/kbn/kbn_test_config.ts similarity index 76% rename from packages/kbn-test/src/kbn/kbn_test_config.js rename to packages/kbn-test/src/kbn/kbn_test_config.ts index c43efabb4b74..909c94098cf5 100644 --- a/packages/kbn-test/src/kbn/kbn_test_config.js +++ b/packages/kbn-test/src/kbn/kbn_test_config.ts @@ -16,26 +16,34 @@ * specific language governing permissions and limitations * under the License. */ - -import { kibanaTestUser } from './users'; import url from 'url'; +import { kibanaTestUser } from './users'; + +interface UrlParts { + protocol?: string; + hostname?: string; + port?: number; + auth?: string; + username?: string; + password?: string; +} export const kbnTestConfig = new (class KbnTestConfig { getPort() { return this.getUrlParts().port; } - getUrlParts() { + getUrlParts(): UrlParts { // allow setting one complete TEST_KIBANA_URL for ES like https://elastic:changeme@example.com:9200 if (process.env.TEST_KIBANA_URL) { const testKibanaUrl = url.parse(process.env.TEST_KIBANA_URL); return { - protocol: testKibanaUrl.protocol.slice(0, -1), + protocol: testKibanaUrl.protocol?.slice(0, -1), hostname: testKibanaUrl.hostname, - port: parseInt(testKibanaUrl.port, 10), + port: testKibanaUrl.port ? parseInt(testKibanaUrl.port, 10) : undefined, auth: testKibanaUrl.auth, - username: testKibanaUrl.auth.split(':')[0], - password: testKibanaUrl.auth.split(':')[1], + username: testKibanaUrl.auth?.split(':')[0], + password: testKibanaUrl.auth?.split(':')[1], }; } @@ -44,7 +52,7 @@ export const kbnTestConfig = new (class KbnTestConfig { return { protocol: process.env.TEST_KIBANA_PROTOCOL || 'http', hostname: process.env.TEST_KIBANA_HOSTNAME || 'localhost', - port: parseInt(process.env.TEST_KIBANA_PORT, 10) || 5620, + port: process.env.TEST_KIBANA_PORT ? parseInt(process.env.TEST_KIBANA_PORT, 10) : 5620, auth: `${username}:${password}`, username, password, diff --git a/packages/kbn-test/src/kbn/users.js b/packages/kbn-test/src/kbn/users.ts similarity index 100% rename from packages/kbn-test/src/kbn/users.js rename to packages/kbn-test/src/kbn/users.ts diff --git a/src/core/TESTING.md b/src/core/TESTING.md index bed41ab58349..a62922d9b5d6 100644 --- a/src/core/TESTING.md +++ b/src/core/TESTING.md @@ -29,6 +29,14 @@ This document outlines best practices and patterns for testing Kibana Plugins. - [Testing dependencies usages](#testing-dependencies-usages) - [Testing components consuming the dependencies](#testing-components-consuming-the-dependencies) - [Testing optional plugin dependencies](#testing-optional-plugin-dependencies) + - [RXJS testing](#rxjs-testing) + - [Testing RXJS observables with marble](#rxjs-testing-with-marble) + - [Precondition](#preconditions-2) + - [Examples](#example-5) + - [Testing an interval based observable](#testing-an-interval-based-observable) + - [Testing observable completion](#testing-observable-completion) + - [Testing observable errors](#testing-observable-errors) + - [Testing promise based observables](#testing-promise-based-observables) ## Strategy @@ -1087,3 +1095,271 @@ describe('Plugin', () => { }); }); ``` + +## RXJS testing + +### Testing RXJS observables with marble + +Testing observable based APIs can be challenging, specially when asynchronous operators or sources are used, +or when trying to assert against emission's timing. + +Fortunately, RXJS comes with it's own `marble` testing module to greatly facilitate that kind of testing. + +See [the official doc](https://rxjs-dev.firebaseapp.com/guide/testing/marble-testing) for more information about marble testing. + +### Preconditions + +The following examples all assume that the following snippet is included in every test file: + +```typescript +import { TestScheduler } from 'rxjs/testing'; + +const getTestScheduler = () => + new TestScheduler((actual, expected) => { + expect(actual).toEqual(expected); + }); +``` + +`getTestScheduler` creates a `TestScheduler` that is wired on `jest`'s `expect` statement when comparing an observable's time frame. + +### Examples + +#### Testing an interval based observable + +Here is a very basic example of an interval-based API: + +```typescript +class FooService { + setup() { + return { + getUpdate$: () => { + return interval(100).pipe(map((count) => `update-${count + 1}`)); + }, + }; + } +} +``` + +If we were to be adding a test that asserts the correct behavior of this API without using marble testing, it +would probably be something like: + +```typescript +it('getUpdate$ emits updates every 100ms', async () => { + const service = new FooService(); + const { getUpdate$ } = service.setup(); + expect(await getUpdate$().pipe(take(3), toArray()).toPromise()).toEqual([ + 'update-1', + 'update-2', + 'update-3', + ]); +}); +``` + +Note that if we are able to test the correct value of each emission, we don't have any way to assert that +the interval of 100ms was respected. Even using a subscription based test to try to do so would result +in potential flakiness, as the subscription execution could trigger on the `101ms` time frame for example. + +It also may be important to note: +- as we need to convert the observable to a promise and wait for the result, the test is `async` +- we need to perform observable transformation (`take` + `toArray`) in the test to have an usable value to assert against. + +Marble testing would allow to get rid of these limitations. An equivalent and improved marble test could be: + +```typescript + describe('getUpdate$', () => { + it('emits updates every 100ms', () => { + getTestScheduler().run(({ expectObservable }) => { + const { getUpdate$ } = service.setup(); + expectObservable(getUpdate$(), '301ms !').toBe('100ms a 99ms b 99ms c', { + a: 'update-1', + b: 'update-2', + c: 'update-3', + }); + }); + }); + }); +``` + +Notes: +- the test is now synchronous +- the second parameter of `expectObservable` (`'301ms !'`) is used to perform manual unsubscription to the observable, as + `interval` never ends. +- an emission is considered a time frame, meaning that after the initial `a` emission, we are at the frame `101`, not `100` + which is why we are then only using a `99ms` gap between a->b and b->c. + +#### Testing observable completion + +Let's 'improve' our `getUpdate$` API by allowing the consumer to manually terminate the observable chain using +a new `abort$` option: + +```typescript +class FooService { + setup() { + return { + // note: using an abortion observable is usually an anti-pattern, as unsubscribing from the observable + // is, most of the time, a better solution. This is only used for the example purpose. + getUpdate$: ({ abort$ = EMPTY }: { abort$?: Observable } = {}) => { + return interval(100).pipe( + takeUntil(abort$), + map((count) => `update-${count + 1}`) + ); + }, + }; + } +} +``` + +We would then add a test to assert than this new option usage is respected: + +```typescript +it('getUpdate$ completes when `abort$` emits', () => { + const service = new FooService(); + getTestScheduler().run(({ expectObservable, hot }) => { + const { getUpdate$ } = service.setup(); + const abort$ = hot('149ms a', { a: undefined }); + expectObservable(getUpdate$({ abort$ })).toBe('100ms a 48ms |', { + a: 'update-1', + }); + }); +}); +``` + +Notes: + - the `|` symbol represents the completion of the observable. + - we are here using the `hot` testing utility to create the `abort$` observable to ensure correct emission timing. + +#### Testing observable errors + +Testing errors thrown by the observable is very close to the previous examples and is done using +the third parameter of `expectObservable`. + +Say we have a service in charge of processing data from an observable and returning the results in a new observable: + +```typescript +interface SomeDataType { + id: string; +} + +class BarService { + setup() { + return { + processDataStream: (data$: Observable) => { + return data$.pipe( + map((data) => { + if (data.id === 'invalid') { + throw new Error(`invalid data: '${data.id}'`); + } + return { + ...data, + processed: 'additional-data', + }; + }) + ); + }, + }; + } +} +``` + +We could write a test that asserts the service properly emit processed results until an invalid data is encountered: + +```typescript +it('processDataStream throw an error when processing invalid data', () => { + getTestScheduler().run(({ expectObservable, hot }) => { + const service = new BarService(); + const { processDataStream } = service.setup(); + + const data = hot('--a--b--(c|)', { + a: { id: 'a' }, + b: { id: 'invalid' }, + c: { id: 'c' }, + }); + + expectObservable(processDataStream(data)).toBe( + '--a--#', + { + a: { id: 'a', processed: 'additional-data' }, + }, + `'[Error: invalid data: 'invalid']'` + ); + }); +}); +``` + +Notes: + - the `-` symbol represents one virtual time frame. + - the `#` symbol represents an error. + - when throwing custom `Error` classes, the assertion can be against an error instance, but this doesn't work + with base errors. + +#### Testing promise based observables + +In some cases, the observable we want to test is based on a Promise (like `of(somePromise).pipe(...)`). This can occur +when using promise-based services, such as core's `http`, for instance. + +```typescript +export const callServerAPI = ( + http: HttpStart, + body: Record, + { abort$ }: { abort$: Observable } +): Observable => { + let controller: AbortController | undefined; + if (abort$) { + controller = new AbortController(); + abort$.subscribe(() => { + controller!.abort(); + }); + } + return from( + http.post('/api/endpoint', { + body, + signal: controller?.signal, + }) + ).pipe( + takeUntil(abort$ ?? EMPTY), + map((response) => response.results) + ); +}; +``` + +Testing that kind of promise based observable does not work out of the box with marble testing, as the asynchronous promise resolution +is not handled by the test scheduler's 'sandbox'. + +Fortunately, there are workarounds for this problem. The most common one being to mock the promise-returning API to return +an observable instead for testing, as `of(observable)` also works and returns the input observable. + +Note that when doing so, the test suite must also include tests using a real promise value to ensure correct behavior in real situation. + +```typescript + +// NOTE: test scheduler do not properly work with promises because of their asynchronous nature. +// we are cheating here by having `http.post` return an observable instead of a promise. +// this still allows more finely grained testing about timing, and asserting that the method +// works properly when `post` returns a real promise is handled in other tests of this suite + +it('callServerAPI result observable emits when the response is received', () => { + const http = httpServiceMock.createStartContract(); + getTestScheduler().run(({ expectObservable, hot }) => { + // need to cast the observable as `any` because http.post.mockReturnValue expects a promise, see previous comment + http.post.mockReturnValue(hot('---(a|)', { a: { someData: 'foo' } }) as any); + + const results = callServerAPI(http, { query: 'term' }, {}); + + expectObservable(results).toBe('---(a|)', { + a: { someData: 'foo' }, + }); + }); +}); + +it('completes without returning results if aborted$ emits before the response', () => { + const http = httpServiceMock.createStartContract(); + getTestScheduler().run(({ expectObservable, hot }) => { + // need to cast the observable as `any` because http.post.mockReturnValue expects a promise, see previous comment + http.post.mockReturnValue(hot('---(a|)', { a: { someData: 'foo' } }) as any); + const aborted$ = hot('-(a|)', { a: undefined }); + const results = callServerAPI(http, { query: 'term' }, { aborted$ }); + + expectObservable(results).toBe('-|'); + }); +}); +``` \ No newline at end of file diff --git a/src/core/public/saved_objects/saved_objects_client.ts b/src/core/public/saved_objects/saved_objects_client.ts index 5c8eca4a33ec..cb279b2cc4c8 100644 --- a/src/core/public/saved_objects/saved_objects_client.ts +++ b/src/core/public/saved_objects/saved_objects_client.ts @@ -303,7 +303,6 @@ export class SavedObjectsClient { query, }); return request.then((resp) => { - resp.saved_objects = resp.saved_objects.map((d) => this.createSavedObject(d)); return renameKeys< PromiseType>, SavedObjectsFindResponsePublic @@ -314,7 +313,10 @@ export class SavedObjectsClient { per_page: 'perPage', page: 'page', }, - resp + { + ...resp, + saved_objects: resp.saved_objects.map((d) => this.createSavedObject(d)), + } ) as SavedObjectsFindResponsePublic; }); }; diff --git a/src/core/server/index.ts b/src/core/server/index.ts index 658c24f83502..dccd58c24a7d 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -217,6 +217,7 @@ export { SavedObjectsErrorHelpers, SavedObjectsExportOptions, SavedObjectsExportResultDetails, + SavedObjectsFindResult, SavedObjectsFindResponse, SavedObjectsImportConflictError, SavedObjectsImportError, diff --git a/src/core/server/saved_objects/export/get_sorted_objects_for_export.test.ts b/src/core/server/saved_objects/export/get_sorted_objects_for_export.test.ts index 32485f461f59..5da2235828b5 100644 --- a/src/core/server/saved_objects/export/get_sorted_objects_for_export.test.ts +++ b/src/core/server/saved_objects/export/get_sorted_objects_for_export.test.ts @@ -47,6 +47,7 @@ describe('getSortedObjectsForExport()', () => { id: '2', type: 'search', attributes: {}, + score: 1, references: [ { name: 'name', @@ -59,6 +60,7 @@ describe('getSortedObjectsForExport()', () => { id: '1', type: 'index-pattern', attributes: {}, + score: 1, references: [], }, ], @@ -133,6 +135,7 @@ describe('getSortedObjectsForExport()', () => { id: '2', type: 'search', attributes: {}, + score: 1, references: [ { name: 'name', @@ -145,6 +148,7 @@ describe('getSortedObjectsForExport()', () => { id: '1', type: 'index-pattern', attributes: {}, + score: 1, references: [], }, ], @@ -192,6 +196,7 @@ describe('getSortedObjectsForExport()', () => { id: '2', type: 'search', attributes: {}, + score: 1, references: [ { name: 'name', @@ -204,6 +209,7 @@ describe('getSortedObjectsForExport()', () => { id: '1', type: 'index-pattern', attributes: {}, + score: 1, references: [], }, ], @@ -279,6 +285,7 @@ describe('getSortedObjectsForExport()', () => { id: '2', type: 'search', attributes: {}, + score: 1, references: [ { name: 'name', @@ -291,6 +298,7 @@ describe('getSortedObjectsForExport()', () => { id: '1', type: 'index-pattern', attributes: {}, + score: 1, references: [], }, ], @@ -366,6 +374,7 @@ describe('getSortedObjectsForExport()', () => { id: '2', type: 'search', attributes: {}, + score: 1, references: [ { type: 'index-pattern', @@ -378,6 +387,7 @@ describe('getSortedObjectsForExport()', () => { id: '1', type: 'index-pattern', attributes: {}, + score: 1, references: [], }, ], @@ -405,6 +415,7 @@ describe('getSortedObjectsForExport()', () => { attributes: { name: 'baz', }, + score: 1, references: [], }, { @@ -413,6 +424,7 @@ describe('getSortedObjectsForExport()', () => { attributes: { name: 'foo', }, + score: 1, references: [], }, { @@ -421,6 +433,7 @@ describe('getSortedObjectsForExport()', () => { attributes: { name: 'bar', }, + score: 1, references: [], }, ], diff --git a/src/core/server/saved_objects/export/get_sorted_objects_for_export.ts b/src/core/server/saved_objects/export/get_sorted_objects_for_export.ts index cafaa5a3147d..6e985c25aeae 100644 --- a/src/core/server/saved_objects/export/get_sorted_objects_for_export.ts +++ b/src/core/server/saved_objects/export/get_sorted_objects_for_export.ts @@ -116,8 +116,11 @@ async function fetchObjectsToExport({ } // sorts server-side by _id, since it's only available in fielddata - return findResponse.saved_objects.sort((a: SavedObject, b: SavedObject) => - a.id > b.id ? 1 : -1 + return ( + findResponse.saved_objects + // exclude the find-specific `score` property from the exported objects + .map(({ score, ...obj }) => obj) + .sort((a: SavedObject, b: SavedObject) => (a.id > b.id ? 1 : -1)) ); } else { throw Boom.badRequest('Either `type` or `objects` are required.'); diff --git a/src/core/server/saved_objects/routes/integration_tests/find.test.ts b/src/core/server/saved_objects/routes/integration_tests/find.test.ts index 31bda1d6b9cb..33e12dd4e517 100644 --- a/src/core/server/saved_objects/routes/integration_tests/find.test.ts +++ b/src/core/server/saved_objects/routes/integration_tests/find.test.ts @@ -79,6 +79,7 @@ describe('GET /api/saved_objects/_find', () => { timeFieldName: '@timestamp', notExpandable: true, attributes: {}, + score: 1, references: [], }, { @@ -88,6 +89,7 @@ describe('GET /api/saved_objects/_find', () => { timeFieldName: '@timestamp', notExpandable: true, attributes: {}, + score: 1, references: [], }, ], diff --git a/src/core/server/saved_objects/service/lib/repository.test.js b/src/core/server/saved_objects/service/lib/repository.test.js index d631ef9cb353..ea749235cbb4 100644 --- a/src/core/server/saved_objects/service/lib/repository.test.js +++ b/src/core/server/saved_objects/service/lib/repository.test.js @@ -1939,7 +1939,7 @@ describe('SavedObjectsRepository', () => { { _index: '.kibana', _id: `${namespace ? `${namespace}:` : ''}config:6.0.0-alpha1`, - _score: 1, + _score: 2, ...mockVersionProps, _source: { namespace, @@ -1954,7 +1954,7 @@ describe('SavedObjectsRepository', () => { { _index: '.kibana', _id: `${namespace ? `${namespace}:` : ''}index-pattern:stocks-*`, - _score: 1, + _score: 3, ...mockVersionProps, _source: { namespace, @@ -1970,7 +1970,7 @@ describe('SavedObjectsRepository', () => { { _index: '.kibana', _id: `${NAMESPACE_AGNOSTIC_TYPE}:something`, - _score: 1, + _score: 4, ...mockVersionProps, _source: { type: NAMESPACE_AGNOSTIC_TYPE, @@ -2131,6 +2131,7 @@ describe('SavedObjectsRepository', () => { type: doc._source.type, ...mockTimestampFields, version: mockVersion, + score: doc._score, attributes: doc._source[doc._source.type], references: [], }); @@ -2153,6 +2154,7 @@ describe('SavedObjectsRepository', () => { type: doc._source.type, ...mockTimestampFields, version: mockVersion, + score: doc._score, attributes: doc._source[doc._source.type], references: [], }); diff --git a/src/core/server/saved_objects/service/lib/repository.ts b/src/core/server/saved_objects/service/lib/repository.ts index 03538f239484..40c5282a77e4 100644 --- a/src/core/server/saved_objects/service/lib/repository.ts +++ b/src/core/server/saved_objects/service/lib/repository.ts @@ -41,6 +41,7 @@ import { SavedObjectsBulkUpdateResponse, SavedObjectsCreateOptions, SavedObjectsFindResponse, + SavedObjectsFindResult, SavedObjectsUpdateOptions, SavedObjectsUpdateResponse, SavedObjectsBulkUpdateObject, @@ -674,8 +675,11 @@ export class SavedObjectsRepository { page, per_page: perPage, total: response.hits.total, - saved_objects: response.hits.hits.map((hit: SavedObjectsRawDoc) => - this._rawToSavedObject(hit) + saved_objects: response.hits.hits.map( + (hit: SavedObjectsRawDoc): SavedObjectsFindResult => ({ + ...this._rawToSavedObject(hit), + score: (hit as any)._score, + }) ), }; } diff --git a/src/core/server/saved_objects/service/saved_objects_client.ts b/src/core/server/saved_objects/service/saved_objects_client.ts index 8780f07cc309..e15a92c92772 100644 --- a/src/core/server/saved_objects/service/saved_objects_client.ts +++ b/src/core/server/saved_objects/service/saved_objects_client.ts @@ -79,6 +79,17 @@ export interface SavedObjectsBulkResponse { saved_objects: Array>; } +/** + * + * @public + */ +export interface SavedObjectsFindResult extends SavedObject { + /** + * The Elasticsearch `_score` of this result. + */ + score: number; +} + /** * Return type of the Saved Objects `find()` method. * @@ -88,7 +99,7 @@ export interface SavedObjectsBulkResponse { * @public */ export interface SavedObjectsFindResponse { - saved_objects: Array>; + saved_objects: Array>; total: number; per_page: number; page: number; diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index ecfa09fbd37f..833c8918a086 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -2039,11 +2039,16 @@ export interface SavedObjectsFindResponse { // (undocumented) per_page: number; // (undocumented) - saved_objects: Array>; + saved_objects: Array>; // (undocumented) total: number; } +// @public (undocumented) +export interface SavedObjectsFindResult extends SavedObject { + score: number; +} + // @public export interface SavedObjectsImportConflictError { // (undocumented) diff --git a/src/es_archiver/es_archiver.ts b/src/es_archiver/es_archiver.ts index f36cbb3f516b..e335652195b8 100644 --- a/src/es_archiver/es_archiver.ts +++ b/src/es_archiver/es_archiver.ts @@ -49,7 +49,7 @@ export class EsArchiver { this.client = client; this.dataDir = dataDir; this.log = log; - this.kbnClient = new KbnClient(log, [kibanaUrl]); + this.kbnClient = new KbnClient(log, { url: kibanaUrl }); } /** diff --git a/src/plugins/saved_objects_management/server/lib/find_all.test.ts b/src/plugins/saved_objects_management/server/lib/find_all.test.ts index 2515d11f6d4b..823a103d8bab 100644 --- a/src/plugins/saved_objects_management/server/lib/find_all.test.ts +++ b/src/plugins/saved_objects_management/server/lib/find_all.test.ts @@ -18,17 +18,18 @@ */ import { times } from 'lodash'; -import { SavedObjectsFindOptions, SavedObject } from 'src/core/server'; +import { SavedObjectsFindOptions, SavedObjectsFindResult } from 'src/core/server'; import { savedObjectsClientMock } from '../../../../core/server/mocks'; import { findAll } from './find_all'; describe('findAll', () => { let savedObjectsClient: ReturnType; - const createObj = (id: number): SavedObject => ({ + const createObj = (id: number): SavedObjectsFindResult => ({ type: 'type', id: `id-${id}`, attributes: {}, + score: 1, references: [], }); diff --git a/src/plugins/saved_objects_management/server/lib/find_relationships.test.ts b/src/plugins/saved_objects_management/server/lib/find_relationships.test.ts index 2c8997c9af21..e18a45d9bdf4 100644 --- a/src/plugins/saved_objects_management/server/lib/find_relationships.test.ts +++ b/src/plugins/saved_objects_management/server/lib/find_relationships.test.ts @@ -77,6 +77,7 @@ describe('findRelationships', () => { type: 'parent-type', id: 'parent-id', attributes: {}, + score: 1, references: [], }, ], diff --git a/test/api_integration/apis/saved_objects/find.js b/test/api_integration/apis/saved_objects/find.js index 7a57d182bc81..7cb5955e4a43 100644 --- a/test/api_integration/apis/saved_objects/find.js +++ b/test/api_integration/apis/saved_objects/find.js @@ -46,6 +46,7 @@ export default function ({ getService }) { attributes: { title: 'Count of requests', }, + score: 0, migrationVersion: resp.body.saved_objects[0].migrationVersion, references: [ { @@ -134,6 +135,7 @@ export default function ({ getService }) { .searchSourceJSON, }, }, + score: 0, references: [ { name: 'kibanaSavedObjectMeta.searchSourceJSON.index', diff --git a/test/api_integration/apis/saved_objects_management/find.ts b/test/api_integration/apis/saved_objects_management/find.ts index e15a9e989d21..4d9f1c165813 100644 --- a/test/api_integration/apis/saved_objects_management/find.ts +++ b/test/api_integration/apis/saved_objects_management/find.ts @@ -56,6 +56,7 @@ export default function ({ getService }: FtrProviderContext) { type: 'index-pattern', }, ], + score: 0, updated_at: '2017-09-21T18:51:23.794Z', meta: { editUrl: diff --git a/test/common/services/kibana_server/kibana_server.ts b/test/common/services/kibana_server/kibana_server.ts index 16039d6fee83..4a251cca044d 100644 --- a/test/common/services/kibana_server/kibana_server.ts +++ b/test/common/services/kibana_server/kibana_server.ts @@ -27,9 +27,9 @@ export function KibanaServerProvider({ getService }: FtrProviderContext) { const config = getService('config'); const lifecycle = getService('lifecycle'); const url = Url.format(config.get('servers.kibana')); + const ssl = config.get('servers.kibana').ssl; const defaults = config.get('uiSettings.defaults'); - - const kbn = new KbnClient(log, [url], defaults); + const kbn = new KbnClient(log, { url, ssl }, defaults); if (defaults) { lifecycle.beforeTests.add(async () => { diff --git a/test/common/services/security/role.ts b/test/common/services/security/role.ts index dfc6ff9b164e..caa5549a70f0 100644 --- a/test/common/services/security/role.ts +++ b/test/common/services/security/role.ts @@ -17,27 +17,20 @@ * under the License. */ -import axios, { AxiosInstance } from 'axios'; import util from 'util'; -import { ToolingLog } from '@kbn/dev-utils'; +import { KbnClient, ToolingLog } from '@kbn/dev-utils'; export class Role { - private log: ToolingLog; - private axios: AxiosInstance; - - constructor(url: string, log: ToolingLog) { - this.log = log; - this.axios = axios.create({ - headers: { 'kbn-xsrf': 'x-pack/ftr/services/security/role' }, - baseURL: url, - maxRedirects: 0, - validateStatus: () => true, // we do our own validation below and throw better error messages - }); - } + constructor(private log: ToolingLog, private kibanaServer: KbnClient) {} public async create(name: string, role: any) { this.log.debug(`creating role ${name}`); - const { data, status, statusText } = await this.axios.put(`/api/security/role/${name}`, role); + const { data, status, statusText } = await this.kibanaServer.request({ + path: `/api/security/role/${name}`, + method: 'PUT', + body: role, + retries: 0, + }); if (status !== 204) { throw new Error( `Expected status code of 204, received ${status} ${statusText}: ${util.inspect(data)}` @@ -47,7 +40,10 @@ export class Role { public async delete(name: string) { this.log.debug(`deleting role ${name}`); - const { data, status, statusText } = await this.axios.delete(`/api/security/role/${name}`); + const { data, status, statusText } = await this.kibanaServer.request({ + path: `/api/security/role/${name}`, + method: 'DELETE', + }); if (status !== 204 && status !== 404) { throw new Error( `Expected status code of 204 or 404, received ${status} ${statusText}: ${util.inspect( diff --git a/test/common/services/security/role_mappings.ts b/test/common/services/security/role_mappings.ts index cc2fa2382549..7951d4b5b47b 100644 --- a/test/common/services/security/role_mappings.ts +++ b/test/common/services/security/role_mappings.ts @@ -17,30 +17,19 @@ * under the License. */ -import axios, { AxiosInstance } from 'axios'; import util from 'util'; -import { ToolingLog } from '@kbn/dev-utils'; +import { KbnClient, ToolingLog } from '@kbn/dev-utils'; export class RoleMappings { - private log: ToolingLog; - private axios: AxiosInstance; - - constructor(url: string, log: ToolingLog) { - this.log = log; - this.axios = axios.create({ - headers: { 'kbn-xsrf': 'x-pack/ftr/services/security/role_mappings' }, - baseURL: url, - maxRedirects: 0, - validateStatus: () => true, // we do our own validation below and throw better error messages - }); - } + constructor(private log: ToolingLog, private kbnClient: KbnClient) {} public async create(name: string, roleMapping: Record) { this.log.debug(`creating role mapping ${name}`); - const { data, status, statusText } = await this.axios.post( - `/internal/security/role_mapping/${name}`, - roleMapping - ); + const { data, status, statusText } = await this.kbnClient.request({ + path: `/internal/security/role_mapping/${name}`, + method: 'POST', + body: roleMapping, + }); if (status !== 200) { throw new Error( `Expected status code of 200, received ${status} ${statusText}: ${util.inspect(data)}` @@ -51,9 +40,10 @@ export class RoleMappings { public async delete(name: string) { this.log.debug(`deleting role mapping ${name}`); - const { data, status, statusText } = await this.axios.delete( - `/internal/security/role_mapping/${name}` - ); + const { data, status, statusText } = await this.kbnClient.request({ + path: `/internal/security/role_mapping/${name}`, + method: 'DELETE', + }); if (status !== 200 && status !== 404) { throw new Error( `Expected status code of 200 or 404, received ${status} ${statusText}: ${util.inspect( diff --git a/test/common/services/security/security.ts b/test/common/services/security/security.ts index 6ad0933a2a5a..fae4c9198cab 100644 --- a/test/common/services/security/security.ts +++ b/test/common/services/security/security.ts @@ -17,8 +17,6 @@ * under the License. */ -import { format as formatUrl } from 'url'; - import { Role } from './role'; import { User } from './user'; import { RoleMappings } from './role_mappings'; @@ -28,14 +26,14 @@ import { createTestUserService } from './test_user'; export async function SecurityServiceProvider(context: FtrProviderContext) { const { getService } = context; const log = getService('log'); - const config = getService('config'); - const url = formatUrl(config.get('servers.kibana')); - const role = new Role(url, log); - const user = new User(url, log); + const kibanaServer = getService('kibanaServer'); + + const role = new Role(log, kibanaServer); + const user = new User(log, kibanaServer); const testUser = await createTestUserService(role, user, context); return new (class SecurityService { - roleMappings = new RoleMappings(url, log); + roleMappings = new RoleMappings(log, kibanaServer); testUser = testUser; role = role; user = user; diff --git a/test/common/services/security/user.ts b/test/common/services/security/user.ts index ae0212704323..58c4d0f1cf34 100644 --- a/test/common/services/security/user.ts +++ b/test/common/services/security/user.ts @@ -17,33 +17,22 @@ * under the License. */ -import axios, { AxiosInstance } from 'axios'; import util from 'util'; -import { ToolingLog } from '@kbn/dev-utils'; +import { KbnClient, ToolingLog } from '@kbn/dev-utils'; export class User { - private log: ToolingLog; - private axios: AxiosInstance; - - constructor(url: string, log: ToolingLog) { - this.log = log; - this.axios = axios.create({ - headers: { 'kbn-xsrf': 'x-pack/ftr/services/security/user' }, - baseURL: url, - maxRedirects: 0, - validateStatus: () => true, // we do our own validation below and throw better error messages - }); - } + constructor(private log: ToolingLog, private kbnClient: KbnClient) {} public async create(username: string, user: any) { this.log.debug(`creating user ${username}`); - const { data, status, statusText } = await this.axios.post( - `/internal/security/users/${username}`, - { + const { data, status, statusText } = await this.kbnClient.request({ + path: `/internal/security/users/${username}`, + method: 'POST', + body: { username, ...user, - } - ); + }, + }); if (status !== 200) { throw new Error( `Expected status code of 200, received ${status} ${statusText}: ${util.inspect(data)}` @@ -54,9 +43,10 @@ export class User { public async delete(username: string) { this.log.debug(`deleting user ${username}`); - const { data, status, statusText } = await this.axios.delete( - `/internal/security/users/${username}` - ); + const { data, status, statusText } = await await this.kbnClient.request({ + path: `/internal/security/users/${username}`, + method: 'DELETE', + }); if (status !== 204) { throw new Error( `Expected status code of 204, received ${status} ${statusText}: ${util.inspect(data)}` diff --git a/test/functional/services/common/browser.ts b/test/functional/services/common/browser.ts index 3297f6e094f7..d6a4fc91481d 100644 --- a/test/functional/services/common/browser.ts +++ b/test/functional/services/common/browser.ts @@ -529,5 +529,10 @@ export async function BrowserProvider({ getService }: FtrProviderContext) { await driver.executeScript('document.body.scrollLeft = ' + scrollSize); return this.getScrollLeft(); } + + public async switchToFrame(idOrElement: number | WebElementWrapper) { + const _id = idOrElement instanceof WebElementWrapper ? idOrElement._webElement : idOrElement; + await driver.switchTo().frame(_id); + } })(); } diff --git a/test/functional/services/remote/remote.ts b/test/functional/services/remote/remote.ts index 5a3a775cae0c..99643929c468 100644 --- a/test/functional/services/remote/remote.ts +++ b/test/functional/services/remote/remote.ts @@ -23,7 +23,7 @@ import { resolve } from 'path'; import { mergeMap } from 'rxjs/operators'; import { FtrProviderContext } from '../../ftr_provider_context'; -import { initWebDriver } from './webdriver'; +import { initWebDriver, BrowserConfig } from './webdriver'; import { Browsers } from './browsers'; export async function RemoteProvider({ getService }: FtrProviderContext) { @@ -58,12 +58,12 @@ export async function RemoteProvider({ getService }: FtrProviderContext) { Fs.writeFileSync(path, JSON.stringify(JSON.parse(coverageJson), null, 2)); }; - const { driver, consoleLog$ } = await initWebDriver( - log, - browserType, - lifecycle, - config.get('browser.logPollingMs') - ); + const browserConfig: BrowserConfig = { + logPollingMs: config.get('browser.logPollingMs'), + acceptInsecureCerts: config.get('browser.acceptInsecureCerts'), + }; + + const { driver, consoleLog$ } = await initWebDriver(log, browserType, lifecycle, browserConfig); const isW3CEnabled = (driver as any).executor_.w3c; const caps = await driver.getCapabilities(); diff --git a/test/functional/services/remote/webdriver.ts b/test/functional/services/remote/webdriver.ts index 9fbbf28bbf42..27814060e70c 100644 --- a/test/functional/services/remote/webdriver.ts +++ b/test/functional/services/remote/webdriver.ts @@ -73,13 +73,18 @@ Executor.prototype.execute = preventParallelCalls( (command: { getName: () => string }) => NO_QUEUE_COMMANDS.includes(command.getName()) ); +export interface BrowserConfig { + logPollingMs: number; + acceptInsecureCerts: boolean; +} + let attemptCounter = 0; let edgePaths: { driverPath: string | undefined; browserPath: string | undefined }; async function attemptToCreateCommand( log: ToolingLog, browserType: Browsers, lifecycle: Lifecycle, - logPollingMs: number + config: BrowserConfig ) { const attemptId = ++attemptCounter; log.debug('[webdriver] Creating session'); @@ -114,6 +119,7 @@ async function attemptToCreateCommand( if (certValidation === '0') { chromeOptions.push('ignore-certificate-errors'); } + if (remoteDebug === '1') { // Visit chrome://inspect in chrome to remotely view/debug chromeOptions.push('headless', 'disable-gpu', 'remote-debugging-port=9222'); @@ -125,6 +131,7 @@ async function attemptToCreateCommand( }); chromeCapabilities.set('unexpectedAlertBehaviour', 'accept'); chromeCapabilities.set('goog:loggingPrefs', { browser: 'ALL' }); + chromeCapabilities.setAcceptInsecureCerts(config.acceptInsecureCerts); const session = await new Builder() .forBrowser(browserType) @@ -137,7 +144,7 @@ async function attemptToCreateCommand( consoleLog$: pollForLogEntry$( session, logging.Type.BROWSER, - logPollingMs, + config.logPollingMs, lifecycle.cleanup.after$ ).pipe( takeUntil(lifecycle.cleanup.after$), @@ -174,7 +181,7 @@ async function attemptToCreateCommand( consoleLog$: pollForLogEntry$( session, logging.Type.BROWSER, - logPollingMs, + config.logPollingMs, lifecycle.cleanup.after$ ).pipe( takeUntil(lifecycle.cleanup.after$), @@ -206,6 +213,7 @@ async function attemptToCreateCommand( 'browser.helperApps.neverAsk.saveToDisk', 'application/comma-separated-values, text/csv, text/plain' ); + firefoxOptions.setAcceptInsecureCerts(config.acceptInsecureCerts); if (headlessBrowser === '1') { // See: https://developer.mozilla.org/en-US/docs/Mozilla/Firefox/Headless_mode @@ -317,7 +325,7 @@ export async function initWebDriver( log: ToolingLog, browserType: Browsers, lifecycle: Lifecycle, - logPollingMs: number + config: BrowserConfig ) { const logger = getLogger('webdriver.http.Executor'); logger.setLevel(logging.Level.FINEST); @@ -348,7 +356,7 @@ export async function initWebDriver( while (true) { const command = await Promise.race([ delay(30 * SECOND), - attemptToCreateCommand(log, browserType, lifecycle, logPollingMs), + attemptToCreateCommand(log, browserType, lifecycle, config), ]); if (!command) { diff --git a/test/scripts/jenkins_xpack_firefox_smoke.sh b/test/scripts/jenkins_xpack_firefox_smoke.sh index fdaee76cafa9..ae924a5e1055 100755 --- a/test/scripts/jenkins_xpack_firefox_smoke.sh +++ b/test/scripts/jenkins_xpack_firefox_smoke.sh @@ -7,4 +7,5 @@ checks-reporter-with-killswitch "X-Pack firefox smoke test" \ --debug --bail \ --kibana-install-dir "$KIBANA_INSTALL_DIR" \ --include-tag "includeFirefox" \ - --config test/functional/config.firefox.js; + --config test/functional/config.firefox.js \ + --config test/functional_embedded/config.firefox.ts; diff --git a/x-pack/legacy/plugins/beats_management/common/config_block_validation.ts b/x-pack/legacy/plugins/beats_management/common/config_block_validation.ts deleted file mode 100644 index f3d1b9164e97..000000000000 --- a/x-pack/legacy/plugins/beats_management/common/config_block_validation.ts +++ /dev/null @@ -1,57 +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; - * you may not use this file except in compliance with the Elastic License. - */ -import * as t from 'io-ts'; -import { PathReporter } from 'io-ts/lib/PathReporter'; -import { isLeft } from 'fp-ts/lib/Either'; -import { configBlockSchemas } from './config_schemas'; -import { ConfigurationBlock, createConfigurationBlockInterface } from './domain_types'; - -export const validateConfigurationBlocks = (configurationBlocks: ConfigurationBlock[]) => { - const validationMap = { - isHosts: t.array(t.string), - isString: t.string, - isPeriod: t.string, - isPath: t.string, - isPaths: t.array(t.string), - isYaml: t.string, - }; - - for (const [index, block] of configurationBlocks.entries()) { - const blockSchema = configBlockSchemas.find((s) => s.id === block.type); - if (!blockSchema) { - throw new Error( - `Invalid config type of ${block.type} used in 'configuration_blocks' at index ${index}` - ); - } - - const interfaceConfig = blockSchema.configs.reduce((props, config) => { - if (config.options) { - props[config.id] = t.keyof( - Object.fromEntries(config.options.map((opt) => [opt.value, null])) as Record - ); - } else if (config.validation) { - props[config.id] = validationMap[config.validation]; - } - - return props; - }, {} as t.Props); - - const runtimeInterface = createConfigurationBlockInterface( - t.literal(blockSchema.id), - t.interface(interfaceConfig) - ); - - const validationResults = runtimeInterface.decode(block); - - if (isLeft(validationResults)) { - throw new Error( - `configuration_blocks validation error, configuration_blocks at index ${index} is invalid. ${ - PathReporter.report(validationResults)[0] - }` - ); - } - } -}; diff --git a/x-pack/legacy/plugins/beats_management/common/config_schemas_translations_map.ts b/x-pack/legacy/plugins/beats_management/common/config_schemas_translations_map.ts index 1aec3e808170..7cae2a85dc4c 100644 --- a/x-pack/legacy/plugins/beats_management/common/config_schemas_translations_map.ts +++ b/x-pack/legacy/plugins/beats_management/common/config_schemas_translations_map.ts @@ -7,7 +7,7 @@ import { i18n } from '@kbn/i18n'; import { ConfigBlockSchema } from './domain_types'; -export const supportedConfigLabelsMap = new Map([ +const supportedConfigLabelsMap = new Map([ [ 'filebeatInputConfig.paths.ui.label', i18n.translate('xpack.beatsManagement.filebeatInputConfig.pathsLabel', { diff --git a/x-pack/legacy/plugins/beats_management/common/domain_types.ts b/x-pack/legacy/plugins/beats_management/common/domain_types.ts index b4a9ac8a0747..32e1d81451c6 100644 --- a/x-pack/legacy/plugins/beats_management/common/domain_types.ts +++ b/x-pack/legacy/plugins/beats_management/common/domain_types.ts @@ -7,8 +7,6 @@ import * as t from 'io-ts'; import { configBlockSchemas } from './config_schemas'; import { DateFromString } from './io_ts_types'; -export const OutputTypesArray = ['elasticsearch', 'logstash', 'kafka', 'redis']; - // Here we create the runtime check for a generic, unknown beat config type. // We can also pass in optional params to create spacific runtime checks that // can be used to validate blocs on the API and UI diff --git a/x-pack/legacy/plugins/beats_management/common/io_ts_types.ts b/x-pack/legacy/plugins/beats_management/common/io_ts_types.ts index d77ad9229869..7d71ea5ad825 100644 --- a/x-pack/legacy/plugins/beats_management/common/io_ts_types.ts +++ b/x-pack/legacy/plugins/beats_management/common/io_ts_types.ts @@ -7,7 +7,7 @@ import * as t from 'io-ts'; import { isRight } from 'fp-ts/lib/Either'; -export class DateFromStringType extends t.Type { +class DateFromStringType extends t.Type { // eslint-disable-next-line public readonly _tag: 'DateFromISOStringType' = 'DateFromISOStringType'; constructor() { diff --git a/x-pack/legacy/plugins/beats_management/common/return_types.ts b/x-pack/legacy/plugins/beats_management/common/return_types.ts index a7125795a5c7..7e0e39e12e60 100644 --- a/x-pack/legacy/plugins/beats_management/common/return_types.ts +++ b/x-pack/legacy/plugins/beats_management/common/return_types.ts @@ -34,11 +34,6 @@ export interface ReturnTypeBulkCreate extends BaseReturnType { }>; } -// delete -export interface ReturnTypeDelete extends BaseReturnType { - action: 'deleted'; -} - export interface ReturnTypeBulkDelete extends BaseReturnType { results: Array<{ success: boolean; @@ -84,12 +79,6 @@ export interface ReturnTypeBulkGet extends BaseReturnType { items: T[]; } -// action -- e.g. validate config block. Like ES simulate endpoint -export interface ReturnTypeAction extends BaseReturnType { - result: { - [key: string]: any; - }; -} // e.g. // { // result: { diff --git a/x-pack/legacy/plugins/beats_management/server/lib/adapters/beats/memory_beats_adapter.ts b/x-pack/legacy/plugins/beats_management/server/lib/adapters/beats/memory_beats_adapter.ts deleted file mode 100644 index afae87c49015..000000000000 --- a/x-pack/legacy/plugins/beats_management/server/lib/adapters/beats/memory_beats_adapter.ts +++ /dev/null @@ -1,118 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { intersection, omit } from 'lodash'; - -import { CMBeat } from '../../../../common/domain_types'; -import { FrameworkUser } from '../framework/adapter_types'; -import { BeatsTagAssignment, CMBeatsAdapter } from './adapter_types'; - -export class MemoryBeatsAdapter implements CMBeatsAdapter { - private beatsDB: CMBeat[]; - - constructor(beatsDB: CMBeat[]) { - this.beatsDB = beatsDB; - } - - public async get(user: FrameworkUser, id: string) { - return this.beatsDB.find((beat) => beat.id === id) || null; - } - - public async insert(user: FrameworkUser, beat: CMBeat) { - this.beatsDB.push(beat); - } - - public async update(user: FrameworkUser, beat: CMBeat) { - const beatIndex = this.beatsDB.findIndex((b) => b.id === beat.id); - - this.beatsDB[beatIndex] = { - ...this.beatsDB[beatIndex], - ...beat, - }; - } - - public async getWithIds(user: FrameworkUser, beatIds: string[]) { - return this.beatsDB.filter((beat) => beatIds.includes(beat.id)); - } - - public async getAllWithTags(user: FrameworkUser, tagIds: string[]): Promise { - return this.beatsDB.filter((beat) => intersection(tagIds, beat.tags || []).length !== 0); - } - - public async getBeatWithToken( - user: FrameworkUser, - enrollmentToken: string - ): Promise { - return this.beatsDB.find((beat) => enrollmentToken === beat.enrollment_token) || null; - } - - public async getAll(user: FrameworkUser) { - return this.beatsDB.map((beat: any) => omit(beat, ['access_token'])); - } - - public async removeTagsFromBeats( - user: FrameworkUser, - removals: BeatsTagAssignment[] - ): Promise { - const beatIds = removals.map((r) => r.beatId); - - const response = this.beatsDB - .filter((beat) => beatIds.includes(beat.id)) - .map((beat) => { - const tagData = removals.find((r) => r.beatId === beat.id); - if (tagData) { - if (beat.tags) { - beat.tags = beat.tags.filter((tag) => tag !== tagData.tag); - } - } - return beat; - }); - - return response.map((item: CMBeat, resultIdx: number) => ({ - idxInRequest: removals[resultIdx].idxInRequest, - result: 'updated', - status: 200, - })); - } - - public async assignTagsToBeats( - user: FrameworkUser, - assignments: BeatsTagAssignment[] - ): Promise { - const beatIds = assignments.map((r) => r.beatId); - - this.beatsDB - .filter((beat) => beatIds.includes(beat.id)) - .map((beat) => { - // get tags that need to be assigned to this beat - const tags = assignments - .filter((a) => a.beatId === beat.id) - .map((t: BeatsTagAssignment) => t.tag); - - if (tags.length > 0) { - if (!beat.tags) { - beat.tags = []; - } - const nonExistingTags = tags.filter((t: string) => beat.tags && !beat.tags.includes(t)); - - if (nonExistingTags.length > 0) { - beat.tags = beat.tags.concat(nonExistingTags); - } - } - return beat; - }); - - return assignments.map((item: BeatsTagAssignment, resultIdx: number) => ({ - idxInRequest: assignments[resultIdx].idxInRequest, - result: 'updated', - status: 200, - })); - } - - public setDB(beatsDB: CMBeat[]) { - this.beatsDB = beatsDB; - } -} diff --git a/x-pack/legacy/plugins/beats_management/server/lib/adapters/configuration_blocks/memory_tags_adapter.ts b/x-pack/legacy/plugins/beats_management/server/lib/adapters/configuration_blocks/memory_tags_adapter.ts deleted file mode 100644 index ea8a75c92fad..000000000000 --- a/x-pack/legacy/plugins/beats_management/server/lib/adapters/configuration_blocks/memory_tags_adapter.ts +++ /dev/null @@ -1,75 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import Chance from 'chance'; // eslint-disable-line -import { ConfigurationBlock } from '../../../../common/domain_types'; -import { FrameworkUser } from '../framework/adapter_types'; -import { ConfigurationBlockAdapter } from './adapter_types'; - -const chance = new Chance(); - -export class MemoryConfigurationBlockAdapter implements ConfigurationBlockAdapter { - private db: ConfigurationBlock[] = []; - - constructor(db: ConfigurationBlock[]) { - this.db = db.map((config) => { - if (config.id === undefined) { - config.id = chance.word(); - } - return config as ConfigurationBlock & { id: string }; - }); - } - - public async getByIds(user: FrameworkUser, ids: string[]) { - return this.db.filter((block) => ids.includes(block.id)); - } - public async delete(user: FrameworkUser, blockIds: string[]) { - this.db = this.db.filter((block) => !blockIds.includes(block.id)); - return blockIds.map((id) => ({ - id, - success: true, - })); - } - public async deleteForTags( - user: FrameworkUser, - tagIds: string[] - ): Promise<{ success: boolean; reason?: string }> { - this.db = this.db.filter((block) => !tagIds.includes(block.tag)); - return { - success: true, - }; - } - - public async getForTags(user: FrameworkUser, tagIds: string[], page?: number, size?: number) { - const results = this.db.filter((block) => tagIds.includes(block.id)); - return { - page: 0, - total: results.length, - blocks: results, - }; - } - - public async create(user: FrameworkUser, blocks: ConfigurationBlock[]) { - return blocks.map((block) => { - const existingIndex = this.db.findIndex((t) => t.id === block.id); - if (existingIndex !== -1) { - this.db[existingIndex] = block; - } else { - this.db.push(block); - } - return block.id; - }); - } - - public setDB(db: ConfigurationBlock[]) { - this.db = db.map((block) => { - if (block.id === undefined) { - block.id = chance.word(); - } - return block; - }); - } -} diff --git a/x-pack/legacy/plugins/beats_management/server/lib/adapters/database/__tests__/kibana.ts b/x-pack/legacy/plugins/beats_management/server/lib/adapters/database/__tests__/kibana.ts deleted file mode 100644 index 460fc412e949..000000000000 --- a/x-pack/legacy/plugins/beats_management/server/lib/adapters/database/__tests__/kibana.ts +++ /dev/null @@ -1,40 +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; - * you may not use this file except in compliance with the Elastic License. - */ -// file.skip - -// @ts-ignore -import { createLegacyEsTestCluster } from '@kbn/test'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { Root } from 'src/core/server/root'; -// @ts-ignore -import * as kbnTestServer from '../../../../../../../../../src/test_utils/kbn_server'; -import { DatabaseKbnESPlugin } from '../adapter_types'; -import { KibanaDatabaseAdapter } from '../kibana_database_adapter'; -import { contractTests } from './test_contract'; -const es = createLegacyEsTestCluster({}); - -let legacyServer: any; -let rootServer: Root; -contractTests('Kibana Database Adapter', { - before: async () => { - await es.start(); - - rootServer = kbnTestServer.createRootWithCorePlugins({ - server: { maxPayloadBytes: 100 }, - }); - - await rootServer.setup(); - legacyServer = kbnTestServer.getKbnServer(rootServer); - return await legacyServer.plugins.elasticsearch.waitUntilReady(); - }, - after: async () => { - await rootServer.shutdown(); - return await es.cleanup(); - }, - adapterSetup: () => { - return new KibanaDatabaseAdapter(legacyServer.plugins.elasticsearch as DatabaseKbnESPlugin); - }, -}); diff --git a/x-pack/legacy/plugins/beats_management/server/lib/adapters/database/__tests__/test_contract.ts b/x-pack/legacy/plugins/beats_management/server/lib/adapters/database/__tests__/test_contract.ts deleted file mode 100644 index 369c2e105621..000000000000 --- a/x-pack/legacy/plugins/beats_management/server/lib/adapters/database/__tests__/test_contract.ts +++ /dev/null @@ -1,58 +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; - * you may not use this file except in compliance with the Elastic License. - */ -import { DatabaseAdapter } from '../adapter_types'; - -interface ContractConfig { - before?(): Promise; - after?(): Promise; - adapterSetup(): DatabaseAdapter; -} - -export const contractTests = (testName: string, config: ContractConfig) => { - describe.skip(testName, () => { - let database: DatabaseAdapter; - beforeAll(async () => { - jest.setTimeout(100000); // 1 second - - if (config.before) { - await config.before(); - } - }); - afterAll(async () => config.after && (await config.after())); - beforeEach(async () => { - database = config.adapterSetup(); - }); - - it('Unauthorized users cant query', async () => { - const params = { - id: `beat:foo`, - ignore: [404], - index: '.management-beats', - }; - let ranWithoutError = false; - try { - await database.get({ kind: 'unauthenticated' }, params); - ranWithoutError = true; - } catch (e) { - expect(e).not.toEqual(null); - } - expect(ranWithoutError).toEqual(false); - }); - - it('Should query ES', async () => { - const params = { - id: `beat:foo`, - ignore: [404], - index: '.management-beats', - }; - const response = await database.get({ kind: 'internal' }, params); - - expect(response).not.toEqual(undefined); - // @ts-ignore - expect(response.found).toEqual(undefined); - }); - }); -}; diff --git a/x-pack/legacy/plugins/beats_management/server/lib/adapters/database/adapter_types.ts b/x-pack/legacy/plugins/beats_management/server/lib/adapters/database/adapter_types.ts index 0a06c3dcc641..90519840af21 100644 --- a/x-pack/legacy/plugins/beats_management/server/lib/adapters/database/adapter_types.ts +++ b/x-pack/legacy/plugins/beats_management/server/lib/adapters/database/adapter_types.ts @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { FrameworkRequest, FrameworkUser } from '../framework/adapter_types'; +import { FrameworkUser } from '../framework/adapter_types'; export interface DatabaseAdapter { get( @@ -39,15 +39,6 @@ export interface DatabaseAdapter { putTemplate(name: string, template: any): Promise; } -export interface DatabaseKbnESCluster { - callWithInternalUser(esMethod: string, options: {}): Promise; - callWithRequest(req: FrameworkRequest, esMethod: string, options: {}): Promise; -} - -export interface DatabaseKbnESPlugin { - getCluster(clusterName: string): DatabaseKbnESCluster; -} - export interface DatabaseSearchParams extends DatabaseGenericParams { analyzer?: string; analyzeWildcard?: boolean; diff --git a/x-pack/legacy/plugins/beats_management/server/lib/adapters/database/kibana_database_adapter.ts b/x-pack/legacy/plugins/beats_management/server/lib/adapters/database/kibana_database_adapter.ts index 1ca3bcae8bfc..baccbe416f39 100644 --- a/x-pack/legacy/plugins/beats_management/server/lib/adapters/database/kibana_database_adapter.ts +++ b/x-pack/legacy/plugins/beats_management/server/lib/adapters/database/kibana_database_adapter.ts @@ -3,6 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { ElasticsearchServiceStart, IClusterClient } from 'src/core/server'; import { FrameworkUser } from '../framework/adapter_types'; import { internalAuthData } from './../framework/adapter_types'; import { @@ -15,8 +16,6 @@ import { DatabaseGetDocumentResponse, DatabaseGetParams, DatabaseIndexDocumentParams, - DatabaseKbnESCluster, - DatabaseKbnESPlugin, DatabaseMGetParams, DatabaseMGetResponse, DatabaseSearchParams, @@ -24,75 +23,67 @@ import { } from './adapter_types'; export class KibanaDatabaseAdapter implements DatabaseAdapter { - private es: DatabaseKbnESCluster; + private es: IClusterClient; - constructor(kbnElasticSearch: DatabaseKbnESPlugin) { - this.es = kbnElasticSearch.getCluster('admin'); + constructor(elasticsearch: ElasticsearchServiceStart) { + this.es = elasticsearch.legacy.client; } public async get( user: FrameworkUser, params: DatabaseGetParams ): Promise> { - const result = await this.callWithUser(user, 'get', params); - return result; - // todo + return await this.callWithUser(user, 'get', params); } public async mget( user: FrameworkUser, params: DatabaseMGetParams ): Promise> { - const result = await this.callWithUser(user, 'mget', params); - return result; - // todo + return await this.callWithUser(user, 'mget', params); } public async bulk(user: FrameworkUser, params: DatabaseBulkIndexDocumentsParams): Promise { - const result = await this.callWithUser(user, 'bulk', params); - return result; + return await this.callWithUser(user, 'bulk', params); } public async create( user: FrameworkUser, params: DatabaseCreateDocumentParams ): Promise { - const result = await this.callWithUser(user, 'create', params); - return result; + return await this.callWithUser(user, 'create', params); } + public async index(user: FrameworkUser, params: DatabaseIndexDocumentParams): Promise { - const result = await this.callWithUser(user, 'index', params); - return result; + return await this.callWithUser(user, 'index', params); } + public async delete( user: FrameworkUser, params: DatabaseDeleteDocumentParams ): Promise { - const result = await this.callWithUser(user, 'delete', params); - return result; + return await this.callWithUser(user, 'delete', params); } public async deleteByQuery( user: FrameworkUser, params: DatabaseSearchParams ): Promise { - const result = await this.callWithUser(user, 'deleteByQuery', params); - return result; + return await this.callWithUser(user, 'deleteByQuery', params); } public async search( user: FrameworkUser, params: DatabaseSearchParams ): Promise> { - const result = await this.callWithUser(user, 'search', params); - return result; + return await this.callWithUser(user, 'search', params); } public async searchAll( user: FrameworkUser, params: DatabaseSearchParams ): Promise> { - const result = await this.callWithUser(user, 'search', { + return await this.callWithUser(user, 'search', { scroll: '1m', ...params, body: { @@ -100,29 +91,24 @@ export class KibanaDatabaseAdapter implements DatabaseAdapter { ...params.body, }, }); - return result; } public async putTemplate(name: string, template: any): Promise { - const result = await this.callWithUser({ kind: 'internal' }, 'indices.putTemplate', { + return await this.callWithUser({ kind: 'internal' }, 'indices.putTemplate', { name, body: template, }); - - return result; } private callWithUser(user: FrameworkUser, esMethod: string, options: any = {}): any { if (user.kind === 'authenticated') { - return this.es.callWithRequest( - { + return this.es + .asScoped({ headers: user[internalAuthData], - } as any, - esMethod, - options - ); + }) + .callAsCurrentUser(esMethod, options); } else if (user.kind === 'internal') { - return this.es.callWithInternalUser(esMethod, options); + return this.es.callAsInternalUser(esMethod, options); } else { throw new Error('Invalid user type'); } diff --git a/x-pack/legacy/plugins/beats_management/server/lib/adapters/events/adapter_types.ts b/x-pack/legacy/plugins/beats_management/server/lib/adapters/events/adapter_types.ts deleted file mode 100644 index 4cb38bb3d057..000000000000 --- a/x-pack/legacy/plugins/beats_management/server/lib/adapters/events/adapter_types.ts +++ /dev/null @@ -1,12 +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; - * you may not use this file except in compliance with the Elastic License. - */ -import { BeatEvent } from '../../../../common/domain_types'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { FrameworkUser } from '../../../../../../../plugins/beats_management/public/lib/adapters/framework/adapter_types'; - -export interface BeatEventsAdapter { - bulkInsert(user: FrameworkUser, beatId: string, events: BeatEvent[]): Promise; -} diff --git a/x-pack/legacy/plugins/beats_management/server/lib/adapters/events/elasticsearch_beat_events_adapter.ts b/x-pack/legacy/plugins/beats_management/server/lib/adapters/events/elasticsearch_beat_events_adapter.ts deleted file mode 100644 index b5056140c8b8..000000000000 --- a/x-pack/legacy/plugins/beats_management/server/lib/adapters/events/elasticsearch_beat_events_adapter.ts +++ /dev/null @@ -1,21 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { BeatEvent } from '../../../../common/domain_types'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { FrameworkUser } from '../../../../../../../plugins/beats_management/public/lib/adapters/framework/adapter_types'; -import { DatabaseAdapter } from '../database/adapter_types'; -import { BeatEventsAdapter } from './adapter_types'; - -export class ElasticsearchBeatEventsAdapter implements BeatEventsAdapter { - // @ts-ignore - constructor(private readonly database: DatabaseAdapter) {} - - // eslint-disable-next-line - public bulkInsert = async (user: FrameworkUser, beatId: string, events: BeatEvent[]) => { - // await this.database.putTemplate(INDEX_NAMES.EVENTS_TODAY, beatsIndexTemplate); - }; -} diff --git a/x-pack/legacy/plugins/beats_management/server/lib/adapters/framework/adapter_types.ts b/x-pack/legacy/plugins/beats_management/server/lib/adapters/framework/adapter_types.ts index 80599f38d982..e2703cb5786d 100644 --- a/x-pack/legacy/plugins/beats_management/server/lib/adapters/framework/adapter_types.ts +++ b/x-pack/legacy/plugins/beats_management/server/lib/adapters/framework/adapter_types.ts @@ -8,6 +8,7 @@ import { Lifecycle, ResponseToolkit } from 'hapi'; import * as t from 'io-ts'; +import { CoreSetup, CoreStart } from 'src/core/server'; import { SecurityPluginSetup } from '../../../../../../../plugins/security/server'; import { LicenseType } from '../../../../common/constants/security'; @@ -33,7 +34,6 @@ export interface BackendFrameworkAdapter { log(text: string): void; on(event: 'xpack.status.green' | 'elasticsearch.status.green', cb: () => void): void; getSetting(settingPath: string): any; - exposeStaticDir(urlPath: string, dir: string): void; registerRoute( route: FrameworkRouteOptions ): void; @@ -42,8 +42,12 @@ export interface BackendFrameworkAdapter { export interface KibanaLegacyServer { newPlatform: { setup: { + core: CoreSetup; plugins: { security: SecurityPluginSetup }; }; + start: { + core: CoreStart; + }; }; plugins: { xpack_main: { diff --git a/x-pack/legacy/plugins/beats_management/server/lib/adapters/framework/hapi_framework_adapter.ts b/x-pack/legacy/plugins/beats_management/server/lib/adapters/framework/hapi_framework_adapter.ts deleted file mode 100644 index 90500e028351..000000000000 --- a/x-pack/legacy/plugins/beats_management/server/lib/adapters/framework/hapi_framework_adapter.ts +++ /dev/null @@ -1,148 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { LicenseType } from './../../../../common/constants/security'; -import { KibanaServerRequest } from './adapter_types'; -import { - BackendFrameworkAdapter, - FrameworkInfo, - FrameworkRequest, - FrameworkResponse, - FrameworkRouteOptions, - internalAuthData, - internalUser, -} from './adapter_types'; - -interface TestSettings { - enrollmentTokensTtlInSeconds: number; - encryptionKey: string; -} - -export class HapiBackendFrameworkAdapter implements BackendFrameworkAdapter { - public info: null | FrameworkInfo = null; - public readonly internalUser = internalUser; - - private settings: TestSettings; - private server: any; - - constructor( - settings: TestSettings = { - encryptionKey: 'something_who_cares', - enrollmentTokensTtlInSeconds: 10 * 60, // 10 minutes - }, - hapiServer?: any, - license: LicenseType = 'trial', - securityEnabled: boolean = true, - licenseActive: boolean = true - ) { - this.server = hapiServer; - this.settings = settings; - const now = new Date(); - - this.info = { - kibana: { - version: 'unknown', - }, - license: { - type: license, - expired: !licenseActive, - expiry_date_in_millis: new Date(now.getFullYear(), now.getMonth() + 1, 1).getTime(), - }, - security: { - enabled: securityEnabled, - available: securityEnabled, - }, - watcher: { - enabled: true, - available: true, - }, - }; - } - public log(text: string) { - this.server.log(text); - } - public on(event: 'xpack.status.green', cb: () => void) { - cb(); - } - public getSetting(settingPath: string) { - switch (settingPath) { - case 'xpack.beats.enrollmentTokensTtlInSeconds': - return this.settings.enrollmentTokensTtlInSeconds; - case 'xpack.beats.encryptionKey': - return this.settings.encryptionKey; - } - } - - public exposeStaticDir(urlPath: string, dir: string): void { - if (!this.server) { - throw new Error('Must pass a hapi server into the adapter to use exposeStaticDir'); - } - this.server.route({ - handler: { - directory: { - path: dir, - }, - }, - method: 'GET', - path: urlPath, - }); - } - - public registerRoute< - RouteRequest extends FrameworkRequest, - RouteResponse extends FrameworkResponse - >(route: FrameworkRouteOptions) { - if (!this.server) { - throw new Error('Must pass a hapi server into the adapter to use registerRoute'); - } - const wrappedHandler = (licenseRequired: string[]) => (request: any, h: any) => { - return route.handler(this.wrapRequest(request), h); - }; - - this.server.route({ - handler: wrappedHandler(route.licenseRequired || []), - method: route.method, - path: route.path, - config: { - ...route.config, - auth: false, - }, - }); - } - - public async injectRequstForTesting({ method, url, headers, payload }: any) { - return await this.server.inject({ method, url, headers, payload }); - } - - private wrapRequest( - req: InternalRequest - ): FrameworkRequest { - const { params, payload, query, headers, info } = req; - - const isAuthenticated = headers.authorization != null; - - return { - user: isAuthenticated - ? { - kind: 'authenticated', - [internalAuthData]: headers, - username: 'elastic', - roles: ['superuser'], - full_name: null, - email: null, - enabled: true, - } - : { - kind: 'unauthenticated', - }, - headers, - info, - params, - payload, - query, - }; - } -} diff --git a/x-pack/legacy/plugins/beats_management/server/lib/adapters/framework/integration_tests/kibana.ts b/x-pack/legacy/plugins/beats_management/server/lib/adapters/framework/integration_tests/kibana.ts deleted file mode 100644 index 4f0ba01b8608..000000000000 --- a/x-pack/legacy/plugins/beats_management/server/lib/adapters/framework/integration_tests/kibana.ts +++ /dev/null @@ -1,38 +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; - * you may not use this file except in compliance with the Elastic License. - */ -// file.skip - -import { camelCase } from 'lodash'; -// @ts-ignore -import * as kbnTestServer from '../../../../../../../../../src/test_utils/kbn_server'; -// @ts-ignore -import { TestKbnServerConfig } from '../../../../../../../test_utils/kbn_server_config'; -import { CONFIG_PREFIX } from '../../../../../common/constants/plugin'; -import { PLUGIN } from './../../../../../common/constants/plugin'; -import { KibanaBackendFrameworkAdapter } from './../kibana_framework_adapter'; -import { contractTests } from './test_contract'; - -let kbnServer: any; -let kbn: any; -let esServer: any; -contractTests('Kibana Framework Adapter', { - async before() { - const servers = kbnTestServer.createTestServers({ - adjustTimeout: (t: number) => jest.setTimeout(t), - settings: TestKbnServerConfig, - }); - esServer = await servers.startES(); - kbn = await servers.startKibana(); - kbnServer = kbn.kbnServer; - }, - async after() { - await kbn.stop(); - await esServer.stop(); - }, - adapterSetup: () => { - return new KibanaBackendFrameworkAdapter(camelCase(PLUGIN.ID), kbnServer.server, CONFIG_PREFIX); - }, -}); diff --git a/x-pack/legacy/plugins/beats_management/server/lib/adapters/framework/integration_tests/test_contract.ts b/x-pack/legacy/plugins/beats_management/server/lib/adapters/framework/integration_tests/test_contract.ts deleted file mode 100644 index 8e21f8cf78ad..000000000000 --- a/x-pack/legacy/plugins/beats_management/server/lib/adapters/framework/integration_tests/test_contract.ts +++ /dev/null @@ -1,29 +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; - * you may not use this file except in compliance with the Elastic License. - */ -import { BackendFrameworkAdapter } from '../adapter_types'; - -interface ContractConfig { - before(): Promise; - after(): Promise; - adapterSetup(): BackendFrameworkAdapter; -} - -export const contractTests = (testName: string, config: ContractConfig) => { - describe(testName, () => { - let frameworkAdapter: BackendFrameworkAdapter; - beforeAll(config.before); - afterAll(config.after); - beforeEach(async () => { - frameworkAdapter = config.adapterSetup(); - }); - - it('Should have tests here', () => { - expect(frameworkAdapter.info).toHaveProperty('server'); - - expect(frameworkAdapter).toHaveProperty('server'); - }); - }); -}; diff --git a/x-pack/legacy/plugins/beats_management/server/lib/adapters/framework/kibana_framework_adapter.ts b/x-pack/legacy/plugins/beats_management/server/lib/adapters/framework/kibana_framework_adapter.ts index 1bf9bbb22b35..3b29e50e4465 100644 --- a/x-pack/legacy/plugins/beats_management/server/lib/adapters/framework/kibana_framework_adapter.ts +++ b/x-pack/legacy/plugins/beats_management/server/lib/adapters/framework/kibana_framework_adapter.ts @@ -68,18 +68,6 @@ export class KibanaBackendFrameworkAdapter implements BackendFrameworkAdapter { this.server.log(text); } - public exposeStaticDir(urlPath: string, dir: string): void { - this.server.route({ - handler: { - directory: { - path: dir, - }, - }, - method: 'GET', - path: urlPath, - }); - } - public registerRoute< RouteRequest extends FrameworkRequest, RouteResponse extends FrameworkResponse diff --git a/x-pack/legacy/plugins/beats_management/server/lib/adapters/tags/memory_tags_adapter.ts b/x-pack/legacy/plugins/beats_management/server/lib/adapters/tags/memory_tags_adapter.ts deleted file mode 100644 index 66a6c7ebebc2..000000000000 --- a/x-pack/legacy/plugins/beats_management/server/lib/adapters/tags/memory_tags_adapter.ts +++ /dev/null @@ -1,50 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { BeatTag } from '../../../../common/domain_types'; -import { FrameworkUser } from '../framework/adapter_types'; -import { CMTagsAdapter } from './adapter_types'; - -export class MemoryTagsAdapter implements CMTagsAdapter { - private tagsDB: BeatTag[] = []; - - constructor(tagsDB: BeatTag[]) { - this.tagsDB = tagsDB; - } - - public async getAll(user: FrameworkUser) { - return this.tagsDB; - } - public async delete(user: FrameworkUser, tagIds: string[]) { - this.tagsDB = this.tagsDB.filter((tag) => !tagIds.includes(tag.id)); - - return true; - } - public async getTagsWithIds(user: FrameworkUser, tagIds: string[]) { - return this.tagsDB.filter((tag) => tagIds.includes(tag.id)); - } - - public async upsertTag(user: FrameworkUser, tag: BeatTag) { - const existingTagIndex = this.tagsDB.findIndex((t) => t.id === tag.id); - if (existingTagIndex !== -1) { - this.tagsDB[existingTagIndex] = tag; - } else { - this.tagsDB.push(tag); - } - return tag.id; - } - - public async getWithoutConfigTypes( - user: FrameworkUser, - blockTypes: string[] - ): Promise { - return this.tagsDB.filter((tag) => tag.hasConfigurationBlocksTypes.includes(blockTypes[0])); - } - - public setDB(tagsDB: BeatTag[]) { - this.tagsDB = tagsDB; - } -} diff --git a/x-pack/legacy/plugins/beats_management/server/lib/adapters/tokens/memory_tokens_adapter.ts b/x-pack/legacy/plugins/beats_management/server/lib/adapters/tokens/memory_tokens_adapter.ts deleted file mode 100644 index 431263c808b4..000000000000 --- a/x-pack/legacy/plugins/beats_management/server/lib/adapters/tokens/memory_tokens_adapter.ts +++ /dev/null @@ -1,49 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { FrameworkAuthenticatedUser, FrameworkUser } from '../framework/adapter_types'; -import { CMTokensAdapter, TokenEnrollmentData } from './adapter_types'; - -export class MemoryTokensAdapter implements CMTokensAdapter { - private tokenDB: TokenEnrollmentData[]; - - constructor(tokenDB: TokenEnrollmentData[]) { - this.tokenDB = tokenDB; - } - - public async deleteEnrollmentToken(user: FrameworkUser, enrollmentToken: string) { - const index = this.tokenDB.findIndex((token) => token.token === enrollmentToken); - - if (index > -1) { - this.tokenDB.splice(index, 1); - } - } - - public async getEnrollmentToken( - user: FrameworkUser, - tokenString: string - ): Promise { - return new Promise((resolve) => { - return resolve(this.tokenDB.find((token) => token.token === tokenString)); - }); - } - - public async insertTokens(user: FrameworkAuthenticatedUser, tokens: TokenEnrollmentData[]) { - tokens.forEach((token) => { - const existingIndex = this.tokenDB.findIndex((t) => t.token === token.token); - if (existingIndex !== -1) { - this.tokenDB[existingIndex] = token; - } else { - this.tokenDB.push(token); - } - }); - return tokens; - } - - public setDB(tokenDB: TokenEnrollmentData[]) { - this.tokenDB = tokenDB; - } -} diff --git a/x-pack/legacy/plugins/beats_management/server/lib/beat_events.ts b/x-pack/legacy/plugins/beats_management/server/lib/beat_events.ts index f4cb3cb424f6..54782783f94c 100644 --- a/x-pack/legacy/plugins/beats_management/server/lib/beat_events.ts +++ b/x-pack/legacy/plugins/beats_management/server/lib/beat_events.ts @@ -6,13 +6,11 @@ import { PathReporter } from 'io-ts/lib/PathReporter'; import { isLeft } from 'fp-ts/lib/Either'; import { BeatEvent, RuntimeBeatEvent } from '../../common/domain_types'; -import { BeatEventsAdapter } from './adapters/events/adapter_types'; import { FrameworkUser } from './adapters/framework/adapter_types'; import { CMBeatsDomain } from './beats'; export class BeatEventsLib { - // @ts-ignore - constructor(private readonly adapter: BeatEventsAdapter, private readonly beats: CMBeatsDomain) {} + constructor(private readonly beats: CMBeatsDomain) {} public log = async ( user: FrameworkUser, diff --git a/x-pack/legacy/plugins/beats_management/server/lib/beats.ts b/x-pack/legacy/plugins/beats_management/server/lib/beats.ts index 3b9c4d35d833..6b7053f40550 100644 --- a/x-pack/legacy/plugins/beats_management/server/lib/beats.ts +++ b/x-pack/legacy/plugins/beats_management/server/lib/beats.ts @@ -7,7 +7,6 @@ import { uniq } from 'lodash'; import moment from 'moment'; import { CMBeat } from '../../common/domain_types'; -import { findNonExistentItems } from '../utils/find_non_existent_items'; import { BeatsRemovalReturn, BeatsTagAssignment, @@ -249,3 +248,12 @@ function addToResultsToResponse(key: string, response: any, assignmentResults: a }); return response; } + +export function findNonExistentItems(items: Array<{ id: string }>, requestedItems: string[]) { + return requestedItems.reduce((nonExistentItems: string[], requestedItem: string, idx: number) => { + if (items.findIndex((item) => item && item.id === requestedItem) === -1) { + nonExistentItems.push(requestedItems[idx]); + } + return nonExistentItems; + }, []); +} diff --git a/x-pack/legacy/plugins/beats_management/server/lib/compose/kibana.ts b/x-pack/legacy/plugins/beats_management/server/lib/compose/kibana.ts index 2bda2fe85d62..b6a645ded816 100644 --- a/x-pack/legacy/plugins/beats_management/server/lib/compose/kibana.ts +++ b/x-pack/legacy/plugins/beats_management/server/lib/compose/kibana.ts @@ -9,9 +9,7 @@ import { PLUGIN } from '../../../common/constants'; import { CONFIG_PREFIX } from '../../../common/constants/plugin'; import { ElasticsearchBeatsAdapter } from '../adapters/beats/elasticsearch_beats_adapter'; import { ElasticsearchConfigurationBlockAdapter } from '../adapters/configuration_blocks/elasticsearch_configuration_block_adapter'; -import { DatabaseKbnESPlugin } from '../adapters/database/adapter_types'; import { KibanaDatabaseAdapter } from '../adapters/database/kibana_database_adapter'; -import { ElasticsearchBeatEventsAdapter } from '../adapters/events/elasticsearch_beat_events_adapter'; import { KibanaLegacyServer } from '../adapters/framework/adapter_types'; import { KibanaBackendFrameworkAdapter } from '../adapters/framework/kibana_framework_adapter'; import { ElasticsearchTagsAdapter } from '../adapters/tags/elasticsearch_tags_adapter'; @@ -28,7 +26,7 @@ export function compose(server: KibanaLegacyServer): CMServerLibs { const framework = new BackendFrameworkLib( new KibanaBackendFrameworkAdapter(camelCase(PLUGIN.ID), server, CONFIG_PREFIX) ); - const database = new KibanaDatabaseAdapter(server.plugins.elasticsearch as DatabaseKbnESPlugin); + const database = new KibanaDatabaseAdapter(server.newPlatform.start.core.elasticsearch); const beatsAdapter = new ElasticsearchBeatsAdapter(database); const configAdapter = new ElasticsearchConfigurationBlockAdapter(database); @@ -46,7 +44,7 @@ export function compose(server: KibanaLegacyServer): CMServerLibs { tokens, framework, }); - const beatEvents = new BeatEventsLib(new ElasticsearchBeatEventsAdapter(database), beats); + const beatEvents = new BeatEventsLib(beats); const libs: CMServerLibs = { beatEvents, diff --git a/x-pack/legacy/plugins/beats_management/server/lib/compose/testing.ts b/x-pack/legacy/plugins/beats_management/server/lib/compose/testing.ts deleted file mode 100644 index b5fe6195fc7c..000000000000 --- a/x-pack/legacy/plugins/beats_management/server/lib/compose/testing.ts +++ /dev/null @@ -1,51 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { MemoryBeatsAdapter } from '../adapters/beats/memory_beats_adapter'; -import { MemoryConfigurationBlockAdapter } from '../adapters/configuration_blocks/memory_tags_adapter'; -import { HapiBackendFrameworkAdapter } from '../adapters/framework/hapi_framework_adapter'; -import { MemoryTagsAdapter } from '../adapters/tags/memory_tags_adapter'; -import { MemoryTokensAdapter } from '../adapters/tokens/memory_tokens_adapter'; -import { BeatEventsLib } from '../beat_events'; -import { CMBeatsDomain } from '../beats'; -import { ConfigurationBlocksLib } from '../configuration_blocks'; -import { BackendFrameworkLib } from '../framework'; -import { CMTagsDomain } from '../tags'; -import { CMTokensDomain } from '../tokens'; -import { CMServerLibs } from '../types'; - -export function compose(server: any): CMServerLibs { - const framework = new BackendFrameworkLib(new HapiBackendFrameworkAdapter(undefined, server)); - - const beatsAdapter = new MemoryBeatsAdapter(server.beatsDB || []); - const configAdapter = new MemoryConfigurationBlockAdapter(server.configsDB || []); - const tags = new CMTagsDomain( - new MemoryTagsAdapter(server.tagsDB || []), - configAdapter, - beatsAdapter - ); - const configurationBlocks = new ConfigurationBlocksLib(configAdapter, tags); - const tokens = new CMTokensDomain(new MemoryTokensAdapter(server.tokensDB || []), { - framework, - }); - const beats = new CMBeatsDomain(beatsAdapter, { - tags, - tokens, - framework, - }); - const beatEvents = new BeatEventsLib({} as any, beats); - - const libs: CMServerLibs = { - beatEvents, - framework, - beats, - tags, - tokens, - configurationBlocks, - }; - - return libs; -} diff --git a/x-pack/legacy/plugins/beats_management/server/lib/framework.ts b/x-pack/legacy/plugins/beats_management/server/lib/framework.ts index 1a6f84a6979c..96a06929073e 100644 --- a/x-pack/legacy/plugins/beats_management/server/lib/framework.ts +++ b/x-pack/legacy/plugins/beats_management/server/lib/framework.ts @@ -16,7 +16,6 @@ import { export class BackendFrameworkLib { public log = this.adapter.log; public on = this.adapter.on.bind(this.adapter); - public exposeStaticDir = this.adapter.exposeStaticDir; public internalUser = this.adapter.internalUser; constructor(private readonly adapter: BackendFrameworkAdapter) { this.validateConfig(); diff --git a/x-pack/legacy/plugins/beats_management/server/rest_api/__tests__/beats_assignments.test.ts b/x-pack/legacy/plugins/beats_management/server/rest_api/__tests__/beats_assignments.test.ts deleted file mode 100644 index 156304443431..000000000000 --- a/x-pack/legacy/plugins/beats_management/server/rest_api/__tests__/beats_assignments.test.ts +++ /dev/null @@ -1,249 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { CMServerLibs } from '../../lib/types'; -import { HapiBackendFrameworkAdapter } from './../../lib/adapters/framework/hapi_framework_adapter'; -import { testHarnes } from './test_harnes'; - -describe('assign_tags_to_beats', () => { - let serverLibs: CMServerLibs; - - beforeAll(async () => { - jest.setTimeout(100000); // 1 second - - serverLibs = await testHarnes.getServerLibs(); - }); - beforeEach(async () => await testHarnes.loadData()); - - it('should add a single tag to a single beat', async () => { - const { result, statusCode } = await ((serverLibs.framework as any) - .adapter as HapiBackendFrameworkAdapter).injectRequstForTesting({ - method: 'POST', - url: '/api/beats/agents_tags/assignments', - headers: { - 'kbn-xsrf': 'xxx', - authorization: 'loggedin', - }, - payload: { - assignments: [{ beatId: 'bar', tag: 'production' }], - }, - }); - - expect(statusCode).toEqual(200); - expect(result.results).toEqual([{ success: true, result: { message: 'updated' } }]); - }); - - it('should not re-add an existing tag to a beat', async () => { - const { result, statusCode } = await ((serverLibs.framework as any) - .adapter as HapiBackendFrameworkAdapter).injectRequstForTesting({ - method: 'POST', - url: '/api/beats/agents_tags/assignments', - headers: { - 'kbn-xsrf': 'xxx', - authorization: 'loggedin', - }, - payload: { - assignments: [{ beatId: 'foo', tag: 'production' }], - }, - }); - - expect(statusCode).toEqual(200); - - expect(result.results).toEqual([{ success: true, result: { message: 'updated' } }]); - - const beat = await serverLibs.beats.getById( - { - kind: 'internal', - }, - 'foo' - ); - expect(beat!.tags).toEqual(['production', 'qa']); // as - }); - - it('should add a single tag to a multiple beats', async () => { - const { result, statusCode } = await ((serverLibs.framework as any) - .adapter as HapiBackendFrameworkAdapter).injectRequstForTesting({ - method: 'POST', - url: '/api/beats/agents_tags/assignments', - headers: { - 'kbn-xsrf': 'xxx', - authorization: 'loggedin', - }, - payload: { - assignments: [ - { beatId: 'foo', tag: 'development' }, - { beatId: 'bar', tag: 'development' }, - ], - }, - }); - - expect(statusCode).toEqual(200); - - expect(result.results).toEqual([ - { success: true, result: { message: 'updated' } }, - { success: true, result: { message: 'updated' } }, - ]); - - let beat; - - beat = await serverLibs.beats.getById( - { - kind: 'internal', - }, - 'foo' - ); - expect(beat!.tags).toEqual(['production', 'qa', 'development']); // as beat 'foo' already had 'production' and 'qa' tags attached to it - - // Beat bar - beat = await serverLibs.beats.getById( - { - kind: 'internal', - }, - 'bar' - ); - - expect(beat!.tags).toEqual(['development']); - }); - - it('should add multiple tags to a single beat', async () => { - const { result, statusCode } = await ((serverLibs.framework as any) - .adapter as HapiBackendFrameworkAdapter).injectRequstForTesting({ - method: 'POST', - url: '/api/beats/agents_tags/assignments', - headers: { - 'kbn-xsrf': 'xxx', - authorization: 'loggedin', - }, - payload: { - assignments: [ - { beatId: 'bar', tag: 'development' }, - { beatId: 'bar', tag: 'production' }, - ], - }, - }); - - expect(statusCode).toEqual(200); - - expect(result.results).toEqual([ - { success: true, result: { message: 'updated' } }, - { success: true, result: { message: 'updated' } }, - ]); - - const beat = await serverLibs.beats.getById( - { - kind: 'internal', - }, - 'bar' - ); - - expect(beat!.tags).toEqual(['development', 'production']); - }); - - // it('should add multiple tags to a multiple beats', async () => { - // const { body: apiResponse } = await supertest - // .post('/api/beats/agents_tags/assignments') - // .set('kbn-xsrf', 'xxx') - // .send({ - // assignments: [{ beatId: 'foo', tag: 'development' }, { beatId: 'bar', tag: 'production' }], - // }) - // .expect(200); - - // expect(apiResponse.assignments).to.eql([ - // { status: 200, result: 'updated' }, - // { status: 200, result: 'updated' }, - // ]); - - // let esResponse; - // let beat; - - // // Beat foo - // esResponse = await es.get({ - // index: ES_INDEX_NAME, - // type: ES_TYPE_NAME, - // id: `beat:foo`, - // }); - - // beat = esResponse._source.beat; - // expect(beat.tags).to.eql(['production', 'qa', 'development']); // as beat 'foo' already had 'production' and 'qa' tags attached to it - - // // Beat bar - // esResponse = await es.get({ - // index: ES_INDEX_NAME, - // type: ES_TYPE_NAME, - // id: `beat:bar`, - // }); - - // beat = esResponse._source.beat; - // expect(beat.tags).to.eql(['production']); - // }); - - // it('should return errors for non-existent beats', async () => { - // const nonExistentBeatId = chance.word(); - - // const { body: apiResponse } = await supertest - // .post('/api/beats/agents_tags/assignments') - // .set('kbn-xsrf', 'xxx') - // .send({ - // assignments: [{ beatId: nonExistentBeatId, tag: 'production' }], - // }) - // .expect(200); - - // expect(apiResponse.assignments).to.eql([ - // { status: 404, result: `Beat ${nonExistentBeatId} not found` }, - // ]); - // }); - - // it('should return errors for non-existent tags', async () => { - // const nonExistentTag = chance.word(); - - // const { body: apiResponse } = await supertest - // .post('/api/beats/agents_tags/assignments') - // .set('kbn-xsrf', 'xxx') - // .send({ - // assignments: [{ beatId: 'bar', tag: nonExistentTag }], - // }) - // .expect(200); - - // expect(apiResponse.assignments).to.eql([ - // { status: 404, result: `Tag ${nonExistentTag} not found` }, - // ]); - - // const esResponse = await es.get({ - // index: ES_INDEX_NAME, - // type: ES_TYPE_NAME, - // id: `beat:bar`, - // }); - - // const beat = esResponse._source.beat; - // expect(beat).to.not.have.property('tags'); - // }); - - // it('should return errors for non-existent beats and tags', async () => { - // const nonExistentBeatId = chance.word(); - // const nonExistentTag = chance.word(); - - // const { body: apiResponse } = await supertest - // .post('/api/beats/agents_tags/assignments') - // .set('kbn-xsrf', 'xxx') - // .send({ - // assignments: [{ beatID: nonExistentBeatId, tag: nonExistentTag }], - // }) - // .expect(200); - - // expect(apiResponse.assignments).to.eql([ - // { status: 404, result: `Beat ${nonExistentBeatId} and tag ${nonExistentTag} not found` }, - // ]); - - // const esResponse = await es.get({ - // index: ES_INDEX_NAME, - // type: ES_TYPE_NAME, - // id: `beat:bar`, - // }); - - // const beat = esResponse._source.beat; - // expect(beat).to.not.have.property('tags'); - // }); -}); diff --git a/x-pack/legacy/plugins/beats_management/server/rest_api/__tests__/data.json b/x-pack/legacy/plugins/beats_management/server/rest_api/__tests__/data.json deleted file mode 100644 index 4ee5a4a7e2d5..000000000000 --- a/x-pack/legacy/plugins/beats_management/server/rest_api/__tests__/data.json +++ /dev/null @@ -1,178 +0,0 @@ -{ - "value": { - "index": ".management-beats", - "type": "_doc", - "id": "beat:qux", - "source": { - "type": "beat", - "beat": { - "type": "filebeat", - "active": true, - "host_ip": "1.2.3.4", - "host_name": "foo.bar.com", - "id": "qux", - "name": "qux_filebeat", - "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjcmVhdGVkIjoiMjAxOC0wNi0zMFQwMzo0MjoxNS4yMzBaIiwiaWF0IjoxNTMwMzMwMTM1fQ.SSsX2Byyo1B1bGxV8C3G4QldhE5iH87EY_1r21-bwbI" - } - } - } -} - -{ - "value": { - "index": ".management-beats", - "type": "_doc", - "id": "beat:baz", - "source": { - "type": "beat", - "beat": { - "type": "metricbeat", - "active": true, - "host_ip": "22.33.11.44", - "host_name": "baz.bar.com", - "id": "baz", - "name": "baz_metricbeat", - "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjcmVhdGVkIjoiMjAxOC0wNi0zMFQwMzo0MjoxNS4yMzBaIiwiaWF0IjoxNTMwMzMwMTM1fQ.SSsX2Byyo1B1bGxV8C3G4QldhE5iH87EY_1r21-bwbI" - } - } - } -} - -{ - "value": { - "index": ".management-beats", - "type": "_doc", - "id": "beat:foo", - "source": { - "type": "beat", - "beat": { - "type": "metricbeat", - "active": true, - "host_ip": "1.2.3.4", - "host_name": "foo.bar.com", - "id": "foo", - "name": "foo_metricbeat", - "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjcmVhdGVkIjoiMjAxOC0wNi0zMFQwMzo0MjoxNS4yMzBaIiwiaWF0IjoxNTMwMzMwMTM1fQ.SSsX2Byyo1B1bGxV8C3G4QldhE5iH87EY_1r21-bwbI", - "verified_on": "2018-05-15T16:25:38.924Z", - "tags": [ - "production", - "qa" - ] - } - } - } -} - -{ - "value": { - "index": ".management-beats", - "type": "_doc", - "id": "beat:bar", - "source": { - "type": "beat", - "beat": { - "type": "filebeat", - "active": true, - "host_ip": "11.22.33.44", - "host_name": "foo.com", - "id": "bar", - "name": "bar_filebeat", - "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjcmVhdGVkIjoiMjAxOC0wNi0zMFQwMzo0MjoxNS4yMzBaIiwiaWF0IjoxNTMwMzMwMTM1fQ.SSsX2Byyo1B1bGxV8C3G4QldhE5iH87EY_1r21-bwbI" - } - } - } -} - -{ - "value": { - "index": ".management-beats", - "type": "_doc", - "id": "tag:production", - "source": { - "type": "tag", - "tag": { - "color": "blue" - } - } - } -} - -{ - "value": { - "index": ".management-beats", - "type": "_doc", - "id": "tag:development", - "source": { - "type": "tag", - "tag": { - "color": "red" - } - } - } -} - -{ - "value": { - "index": ".management-beats", - "type": "_doc", - "id": "tag:qa", - "source": { - "type": "tag", - "tag": { - "color": "green" - } - } - } -} - -{ - "value": { - "index": ".management-beats", - "type": "_doc", - "id": "configuration_block:SDfsdfIBdsfsf50zbta", - "source": { - "type": "configuration_block", - "configuration_block": { - "type": "output", - "description": "some description", - "tag": "production", - "last_updated": "2018-05-15T16:25:38.924Z", - "config": "{ \"username\": \"some-username\", \"hosts\": [\"localhost:11211\"] }" - } - } - } -} - -{ - "value": { - "index": ".management-beats", - "type": "_doc", - "id": "configuration_block:W0tpsmIBdsfsf50zbta", - "source": { - "type": "configuration_block", - "configuration_block": { - "type": "metricbeat.modules", - "tag": "production", - "last_updated": "2018-05-15T16:25:38.924Z", - "config": "{ \"module\": \"memcached\", \"hosts\": [\"localhost:11211\"] }" - } - } - } -} - -{ - "value": { - "index": ".management-beats", - "type": "_doc", - "id": "configuration_block:W0tpsmIBdwcYyG50zbta", - "source": { - "type": "configuration_block", - "configuration_block": { - "type": "metricbeat.modules", - "tag": "qa", - "last_updated": "2018-05-15T16:25:38.924Z", - "config": "{\"module\": \"memcached\", \"node.namespace\": \"node\", \"hosts\": [\"localhost:4949\"] }" - } - } - } -} \ No newline at end of file diff --git a/x-pack/legacy/plugins/beats_management/server/rest_api/__tests__/test_harnes.ts b/x-pack/legacy/plugins/beats_management/server/rest_api/__tests__/test_harnes.ts deleted file mode 100644 index 590ce0bd7b28..000000000000 --- a/x-pack/legacy/plugins/beats_management/server/rest_api/__tests__/test_harnes.ts +++ /dev/null @@ -1,102 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { badRequest } from 'boom'; -import { readFile } from 'fs'; -// @ts-ignore -import Hapi from 'hapi'; -import { resolve } from 'path'; -import { promisify } from 'util'; -import { BeatTag, CMBeat } from '../../../common/domain_types'; -import { TokenEnrollmentData } from '../../lib/adapters/tokens/adapter_types'; -import { compose } from '../../lib/compose/testing'; -import { CMServerLibs } from '../../lib/types'; -import { initManagementServer } from './../../management_server'; - -const readFileAsync = promisify(readFile); -let serverLibs: CMServerLibs; - -export const testHarnes = { - description: 'API Development Tests', - loadData: async () => { - if (!serverLibs) { - throw new Error('Server libs not composed yet...'); - } - const contents = await readFileAsync(resolve(__dirname, './data.json'), 'utf8'); - const database = contents.split(/\n\n/); - - // @ts-ignore the private access - serverLibs.beats.adapter.setDB( - database.reduce((inserts: CMBeat[], source) => { - const type = 'beat'; - const data = JSON.parse(source); - - if (data.value.source.type === type) { - inserts.push({ - id: data.value.id.substring(data.value.id.indexOf(':') + 1), - ...data.value.source[type], - }); - } - return inserts; - }, []) - ); - - // @ts-ignore the private access - serverLibs.tags.adapter.setDB( - database.reduce((inserts: BeatTag[], source) => { - const type = 'tag'; - const data = JSON.parse(source); - - if (data.value.source.type === type) { - inserts.push({ - id: data.value.id.substring(data.value.id.indexOf(':') + 1), - ...data.value.source[type], - }); - } - return inserts; - }, []) - ); - - // @ts-ignore the private access - serverLibs.tokens.adapter.setDB( - database.reduce((inserts: TokenEnrollmentData[], source) => { - const type = 'token'; - const data = JSON.parse(source); - - if (data.value.source.type === type) { - inserts.push({ - id: data.value.id.substring(data.value.id.indexOf(':') + 1), - ...data.value.source[type], - }); - } - return inserts; - }, []) - ); - }, - getServerLibs: async () => { - if (!serverLibs) { - const server = new Hapi.Server({ port: 111111 }); - const versionHeader = 'kbn-version'; - const xsrfHeader = 'kbn-xsrf'; - - server.ext('onPostAuth', (req: any, h: any) => { - const isSafeMethod = req.method === 'get' || req.method === 'head'; - const hasVersionHeader = versionHeader in req.headers; - const hasXsrfHeader = xsrfHeader in req.headers; - - if (!isSafeMethod && !hasVersionHeader && !hasXsrfHeader) { - throw badRequest(`Request must contain a ${xsrfHeader} header.`); - } - - return h.continue; - }); - - serverLibs = compose(server); - initManagementServer(serverLibs); - } - return serverLibs; - }, -}; diff --git a/x-pack/legacy/plugins/beats_management/server/utils/README.md b/x-pack/legacy/plugins/beats_management/server/utils/README.md deleted file mode 100644 index 8a6a27aa2986..000000000000 --- a/x-pack/legacy/plugins/beats_management/server/utils/README.md +++ /dev/null @@ -1 +0,0 @@ -Utils should be data processing functions and other tools.... all in all utils is basicly everything that is not an adaptor, or presenter and yet too much to put in a lib. \ No newline at end of file diff --git a/x-pack/legacy/plugins/beats_management/server/utils/find_non_existent_items.ts b/x-pack/legacy/plugins/beats_management/server/utils/find_non_existent_items.ts deleted file mode 100644 index 0e9b4f0b6fa5..000000000000 --- a/x-pack/legacy/plugins/beats_management/server/utils/find_non_existent_items.ts +++ /dev/null @@ -1,19 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -interface RandomItem { - id: string; - [key: string]: any; -} - -export function findNonExistentItems(items: RandomItem[], requestedItems: any) { - return requestedItems.reduce((nonExistentItems: string[], requestedItem: string, idx: number) => { - if (items.findIndex((item: RandomItem) => item && item.id === requestedItem) === -1) { - nonExistentItems.push(requestedItems[idx]); - } - return nonExistentItems; - }, []); -} diff --git a/x-pack/legacy/plugins/beats_management/server/utils/helper_types.ts b/x-pack/legacy/plugins/beats_management/server/utils/helper_types.ts deleted file mode 100644 index 96f7b7bc79b6..000000000000 --- a/x-pack/legacy/plugins/beats_management/server/utils/helper_types.ts +++ /dev/null @@ -1,13 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -export type InterfaceExcept = Pick>; - -export function arrayFromEnum(e: any): T[] { - return Object.keys(e) - .filter((key) => isNaN(+key)) - .map((name) => e[name]) as T[]; -} diff --git a/x-pack/legacy/plugins/beats_management/server/utils/index_templates/beats_template.json b/x-pack/legacy/plugins/beats_management/server/utils/index_templates/beats_template.json deleted file mode 100644 index ba3a0aba6c25..000000000000 --- a/x-pack/legacy/plugins/beats_management/server/utils/index_templates/beats_template.json +++ /dev/null @@ -1,119 +0,0 @@ -{ - "index_patterns": [".management-beats"], - "version": 66000, - "settings": { - "index": { - "number_of_shards": 1, - "auto_expand_replicas": "0-1", - "codec": "best_compression" - } - }, - "mappings": { - "_doc": { - "dynamic": "strict", - "properties": { - "type": { - "type": "keyword" - }, - "configuration_block": { - "properties": { - "id": { - "type": "keyword" - }, - "type": { - "type": "keyword" - }, - "tag": { - "type": "keyword" - }, - "description": { - "type": "text" - }, - "config": { - "type": "keyword" - }, - "last_updated": { - "type": "date" - } - } - }, - "enrollment_token": { - "properties": { - "token": { - "type": "keyword" - }, - "expires_on": { - "type": "date" - } - } - }, - "tag": { - "properties": { - "id": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, - "color": { - "type": "keyword" - }, - "hasConfigurationBlocksTypes": { - "type": "keyword" - } - } - }, - "beat": { - "properties": { - "id": { - "type": "keyword" - }, - "config_status": { - "type": "keyword" - }, - "active": { - "type": "boolean" - }, - "last_checkin": { - "type": "date" - }, - "enrollment_token": { - "type": "keyword" - }, - "access_token": { - "type": "keyword" - }, - "verified_on": { - "type": "date" - }, - "type": { - "type": "keyword" - }, - "version": { - "type": "keyword" - }, - "host_ip": { - "type": "ip" - }, - "host_name": { - "type": "keyword" - }, - "ephemeral_id": { - "type": "keyword" - }, - "tags": { - "type": "keyword" - }, - "metadata": { - "dynamic": "true", - "type": "object" - }, - "name": { - "type": "keyword" - } - } - } - } - } - } -} diff --git a/x-pack/legacy/plugins/beats_management/server/utils/polyfills.ts b/x-pack/legacy/plugins/beats_management/server/utils/polyfills.ts deleted file mode 100644 index 5291e2c72be7..000000000000 --- a/x-pack/legacy/plugins/beats_management/server/utils/polyfills.ts +++ /dev/null @@ -1,17 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -export const entries = (obj: any) => { - const ownProps = Object.keys(obj); - let i = ownProps.length; - const resArray = new Array(i); // preallocate the Array - - while (i--) { - resArray[i] = [ownProps[i], obj[ownProps[i]]]; - } - - return resArray; -}; diff --git a/x-pack/legacy/plugins/beats_management/server/utils/wrap_request.ts b/x-pack/legacy/plugins/beats_management/server/utils/wrap_request.ts deleted file mode 100644 index 57cf70a99a29..000000000000 --- a/x-pack/legacy/plugins/beats_management/server/utils/wrap_request.ts +++ /dev/null @@ -1,36 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { - FrameworkRequest, - internalAuthData, - KibanaServerRequest, -} from '../lib/adapters/framework/adapter_types'; - -export function wrapRequest( - req: InternalRequest -): FrameworkRequest { - const { params, payload, query, headers, info } = req; - - const isAuthenticated = headers.authorization != null; - - return { - // @ts-ignore -- partial applucation, adapter adds other user data - user: isAuthenticated - ? { - kind: 'authenticated', - [internalAuthData]: headers, - } - : { - kind: 'unauthenticated', - }, - headers, - info, - params, - payload, - query, - }; -} diff --git a/x-pack/legacy/plugins/beats_management/types/eui.d.ts b/x-pack/legacy/plugins/beats_management/types/eui.d.ts deleted file mode 100644 index 636d0a2f7b51..000000000000 --- a/x-pack/legacy/plugins/beats_management/types/eui.d.ts +++ /dev/null @@ -1,16 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -/** - * /!\ These type definitions are temporary until the upstream @elastic/eui - * package includes them. - */ - -import * as eui from '@elastic/eui'; -import { Moment } from 'moment'; -import { ChangeEventHandler, MouseEventHandler, ReactType, Ref, FC } from 'react'; - -declare module '@elastic/eui' {} diff --git a/x-pack/legacy/plugins/beats_management/wallaby.js b/x-pack/legacy/plugins/beats_management/wallaby.js deleted file mode 100644 index 823f63b15bcb..000000000000 --- a/x-pack/legacy/plugins/beats_management/wallaby.js +++ /dev/null @@ -1,62 +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; - * you may not use this file except in compliance with the Elastic License. - */ -const path = require('path'); -process.env.NODE_PATH = path.resolve(__dirname, '..', '..', '..', 'node_modules'); - -module.exports = function (wallaby) { - return { - debug: true, - files: [ - './tsconfig.json', - //'plugins/beats/public/**/*.+(js|jsx|ts|tsx|json|snap|css|less|sass|scss|jpg|jpeg|gif|png|svg)', - 'server/**/*.+(js|jsx|ts|tsx|json|snap|css|less|sass|scss|jpg|jpeg|gif|png|svg)', - 'common/**/*.+(js|jsx|ts|tsx|json|snap|css|less|sass|scss|jpg|jpeg|gif|png|svg)', - 'public/**/*.+(js|jsx|ts|tsx|json|snap|css|less|sass|scss|jpg|jpeg|gif|png|svg)', - '!**/*.test.ts', - ], - - tests: ['**/*.test.ts', '**/*.test.tsx'], - env: { - type: 'node', - runner: 'node', - }, - testFramework: { - type: 'jest', - //path: jestPath, - }, - compilers: { - '**/*.ts?(x)': wallaby.compilers.typeScript({ - typescript: require('typescript'), // eslint-disable-line - }), - '**/*.js': wallaby.compilers.babel({ - babelrc: false, - presets: [require.resolve('@kbn/babel-preset/node_preset')], - }), - }, - - setup: (wallaby) => { - const path = require('path'); - - const kibanaDirectory = path.resolve(wallaby.localProjectDir, '..', '..', '..'); - wallaby.testFramework.configure({ - rootDir: wallaby.localProjectDir, - moduleNameMapper: { - '^ui/(.*)': `${kibanaDirectory}/src/legacy/ui/public/$1`, - // eslint-disable-next-line - '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': `${kibanaDirectory}/src/dev/jest/mocks/file_mock.js`, - '\\.(css|less|scss)$': `${kibanaDirectory}/src/dev/jest/mocks/style_mock.js`, - }, - testURL: 'http://localhost', - setupFiles: [`${kibanaDirectory}/x-pack/dev-tools/jest/setup/enzyme.js`], - snapshotSerializers: [`${kibanaDirectory}/node_modules/enzyme-to-json/serializer`], - transform: { - '^.+\\.js$': `${kibanaDirectory}/src/dev/jest/babel_transform.js`, - //"^.+\\.tsx?$": `${kibanaDirectory}/src/dev/jest/ts_transform.js`, - }, - }); - }, - }; -}; diff --git a/x-pack/plugins/actions/server/actions_client.test.ts b/x-pack/plugins/actions/server/actions_client.test.ts index 2865bbbe1d94..69fab828e63d 100644 --- a/x-pack/plugins/actions/server/actions_client.test.ts +++ b/x-pack/plugins/actions/server/actions_client.test.ts @@ -386,6 +386,7 @@ describe('getAll()', () => { foo: 'bar', }, }, + score: 1, references: [], }, ], diff --git a/x-pack/plugins/alerts/server/alerts_client.test.ts b/x-pack/plugins/alerts/server/alerts_client.test.ts index 9685f58b8fb3..f494f1358980 100644 --- a/x-pack/plugins/alerts/server/alerts_client.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client.test.ts @@ -1667,6 +1667,7 @@ describe('find()', () => { }, ], }, + score: 1, references: [ { name: 'action_0', diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.tsx b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.tsx index 796f2992236f..d71d5f2cb480 100644 --- a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.tsx @@ -5,9 +5,12 @@ */ import { EuiTitle } from '@elastic/eui'; +import theme from '@elastic/eui/dist/eui_theme_light.json'; +import numeral from '@elastic/numeral'; import { i18n } from '@kbn/i18n'; -import { scaleUtc } from 'd3-scale'; import d3 from 'd3'; +import { scaleUtc } from 'd3-scale'; +import mean from 'lodash.mean'; import React from 'react'; import { asRelativeDateTimeRange } from '../../../../utils/formatters'; import { getTimezoneOffsetInMs } from '../../../shared/charts/CustomPlot/getTimezoneOffsetInMs'; @@ -17,7 +20,7 @@ import { EmptyMessage } from '../../../shared/EmptyMessage'; interface IBucket { key: number; - count: number; + count: number | undefined; } // TODO: cleanup duplication of this in distribution/get_distribution.ts (ErrorDistributionAPIResponse) and transactions/distribution/index.ts (TransactionDistributionAPIResponse) @@ -30,7 +33,7 @@ interface IDistribution { interface FormattedBucket { x0: number; x: number; - y: number; + y: number | undefined; } export function getFormattedBuckets( @@ -64,7 +67,7 @@ export function ErrorDistribution({ distribution, title }: Props) { distribution.bucketSize ); - if (!buckets || distribution.noHits) { + if (!buckets) { return ( bucket.y)) || 0; const xMin = d3.min(buckets, (d) => d.x0); const xMax = d3.max(buckets, (d) => d.x); const tickFormat = scaleUtc().domain([xMin, xMax]).tickFormat(); @@ -84,6 +88,7 @@ export function ErrorDistribution({ distribution, title }: Props) { {title} bucket.x} xType="time-utc" @@ -105,6 +110,17 @@ export function ErrorDistribution({ distribution, title }: Props) { values: { occCount: value }, }) } + legends={[ + { + color: theme.euiColorVis1, + // 0a abbreviates large whole numbers with metric prefixes like: 1000 = 1k, 32000 = 32k, 1000000 = 1m + legendValue: numeral(averageValue).format('0a'), + title: i18n.translate('xpack.apm.errorGroupDetails.avgLabel', { + defaultMessage: 'Avg.', + }), + legendClickDisabled: true, + }, + ]} /> ); diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/index.tsx b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/index.tsx index d8885ec11c51..225e5ef2f6ca 100644 --- a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/index.tsx @@ -26,6 +26,9 @@ import { ErrorDistribution } from './Distribution'; import { useLocation } from '../../../hooks/useLocation'; import { useUrlParams } from '../../../hooks/useUrlParams'; import { useTrackPageview } from '../../../../../observability/public'; +import { callApmApi } from '../../../services/rest/createCallApmApi'; +import { ErrorRateChart } from '../../shared/charts/ErrorRateChart'; +import { ChartsSyncContextProvider } from '../../../context/ChartsSyncContext'; const Titles = styled.div` margin-bottom: ${px(units.plus)}; @@ -61,49 +64,43 @@ export function ErrorGroupDetails() { const { urlParams, uiFilters } = useUrlParams(); const { serviceName, start, end, errorGroupId } = urlParams; - const { data: errorGroupData } = useFetcher( - (callApmApi) => { - if (serviceName && start && end && errorGroupId) { - return callApmApi({ - pathname: '/api/apm/services/{serviceName}/errors/{groupId}', - params: { - path: { - serviceName, - groupId: errorGroupId, - }, - query: { - start, - end, - uiFilters: JSON.stringify(uiFilters), - }, + const { data: errorGroupData } = useFetcher(() => { + if (serviceName && start && end && errorGroupId) { + return callApmApi({ + pathname: '/api/apm/services/{serviceName}/errors/{groupId}', + params: { + path: { + serviceName, + groupId: errorGroupId, }, - }); - } - }, - [serviceName, start, end, errorGroupId, uiFilters] - ); - - const { data: errorDistributionData } = useFetcher( - (callApmApi) => { - if (serviceName && start && end && errorGroupId) { - return callApmApi({ - pathname: '/api/apm/services/{serviceName}/errors/distribution', - params: { - path: { - serviceName, - }, - query: { - start, - end, - groupId: errorGroupId, - uiFilters: JSON.stringify(uiFilters), - }, + query: { + start, + end, + uiFilters: JSON.stringify(uiFilters), }, - }); - } - }, - [serviceName, start, end, errorGroupId, uiFilters] - ); + }, + }); + } + }, [serviceName, start, end, errorGroupId, uiFilters]); + + const { data: errorDistributionData } = useFetcher(() => { + if (serviceName && start && end && errorGroupId) { + return callApmApi({ + pathname: '/api/apm/services/{serviceName}/errors/distribution', + params: { + path: { + serviceName, + }, + query: { + start, + end, + groupId: errorGroupId, + uiFilters: JSON.stringify(uiFilters), + }, + }, + }); + } + }, [serviceName, start, end, errorGroupId, uiFilters]); useTrackPageview({ app: 'apm', path: 'error_group_details' }); useTrackPageview({ app: 'apm', path: 'error_group_details', delay: 15000 }); @@ -185,16 +182,24 @@ export function ErrorGroupDetails() { )} - - + + + + + + + + + + {showDetails && ( diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/index.tsx b/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/index.tsx index ff031c5a86d1..73474208e26c 100644 --- a/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/index.tsx @@ -13,64 +13,61 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { useMemo } from 'react'; -import { useFetcher } from '../../../hooks/useFetcher'; -import { ErrorDistribution } from '../ErrorGroupDetails/Distribution'; -import { ErrorGroupList } from './List'; -import { useUrlParams } from '../../../hooks/useUrlParams'; import { useTrackPageview } from '../../../../../observability/public'; import { PROJECTION } from '../../../../common/projections/typings'; +import { useFetcher } from '../../../hooks/useFetcher'; +import { useUrlParams } from '../../../hooks/useUrlParams'; +import { callApmApi } from '../../../services/rest/createCallApmApi'; +import { ErrorRateChart } from '../../shared/charts/ErrorRateChart'; import { LocalUIFilters } from '../../shared/LocalUIFilters'; +import { ErrorDistribution } from '../ErrorGroupDetails/Distribution'; +import { ErrorGroupList } from './List'; +import { ChartsSyncContextProvider } from '../../../context/ChartsSyncContext'; const ErrorGroupOverview: React.FC = () => { const { urlParams, uiFilters } = useUrlParams(); const { serviceName, start, end, sortField, sortDirection } = urlParams; - const { data: errorDistributionData } = useFetcher( - (callApmApi) => { - if (serviceName && start && end) { - return callApmApi({ - pathname: '/api/apm/services/{serviceName}/errors/distribution', - params: { - path: { - serviceName, - }, - query: { - start, - end, - uiFilters: JSON.stringify(uiFilters), - }, + const { data: errorDistributionData } = useFetcher(() => { + if (serviceName && start && end) { + return callApmApi({ + pathname: '/api/apm/services/{serviceName}/errors/distribution', + params: { + path: { + serviceName, }, - }); - } - }, - [serviceName, start, end, uiFilters] - ); + query: { + start, + end, + uiFilters: JSON.stringify(uiFilters), + }, + }, + }); + } + }, [serviceName, start, end, uiFilters]); - const { data: errorGroupListData } = useFetcher( - (callApmApi) => { - const normalizedSortDirection = sortDirection === 'asc' ? 'asc' : 'desc'; + const { data: errorGroupListData } = useFetcher(() => { + const normalizedSortDirection = sortDirection === 'asc' ? 'asc' : 'desc'; - if (serviceName && start && end) { - return callApmApi({ - pathname: '/api/apm/services/{serviceName}/errors', - params: { - path: { - serviceName, - }, - query: { - start, - end, - sortField, - sortDirection: normalizedSortDirection, - uiFilters: JSON.stringify(uiFilters), - }, + if (serviceName && start && end) { + return callApmApi({ + pathname: '/api/apm/services/{serviceName}/errors', + params: { + path: { + serviceName, }, - }); - } - }, - [serviceName, start, end, sortField, sortDirection, uiFilters] - ); + query: { + start, + end, + sortField, + sortDirection: normalizedSortDirection, + uiFilters: JSON.stringify(uiFilters), + }, + }, + }); + } + }, [serviceName, start, end, sortField, sortDirection, uiFilters]); useTrackPageview({ app: 'apm', @@ -102,20 +99,27 @@ const ErrorGroupOverview: React.FC = () => { - - - - - - + + + + + + + + + + + + + diff --git a/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/Legends.js b/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/Legends.js index bf6cf083e00e..87ab81e738eb 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/Legends.js +++ b/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/Legends.js @@ -105,7 +105,9 @@ export default function Legends({ return ( clickLegend(i)} + onClick={ + serie.legendClickDisabled ? undefined : () => clickLegend(i) + } disabled={seriesEnabledState[i]} text={text} color={serie.color} diff --git a/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/index.js b/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/index.js index e1ffec3a8d97..7e74961e57ea 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/index.js +++ b/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/index.js @@ -144,7 +144,7 @@ export class InnerCustomPlot extends PureComponent { const hasValidCoordinates = flatten(series.map((s) => s.data)).some((p) => isValidCoordinateValue(p.y) ); - const noHits = !hasValidCoordinates; + const noHits = this.props.noHits || !hasValidCoordinates; const plotValues = this.getPlotValues({ visibleSeries, @@ -234,6 +234,7 @@ InnerCustomPlot.propTypes = { firstSeen: PropTypes.number, }) ), + noHits: PropTypes.bool, }; InnerCustomPlot.defaultProps = { @@ -241,6 +242,8 @@ InnerCustomPlot.defaultProps = { tickFormatX: undefined, tickFormatY: (y) => y, truncateLegends: false, + xAxisTickSizeOuter: 0, + noHits: false, }; export default makeWidthFlexible(InnerCustomPlot); diff --git a/x-pack/plugins/apm/public/components/shared/charts/ErrorRateChart/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/ErrorRateChart/index.tsx new file mode 100644 index 000000000000..7aafa9e1fdce --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/charts/ErrorRateChart/index.tsx @@ -0,0 +1,100 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { EuiTitle } from '@elastic/eui'; +import theme from '@elastic/eui/dist/eui_theme_light.json'; +import { i18n } from '@kbn/i18n'; +import mean from 'lodash.mean'; +import React, { useCallback } from 'react'; +import { useChartsSync } from '../../../../hooks/useChartsSync'; +import { useFetcher } from '../../../../hooks/useFetcher'; +import { useUrlParams } from '../../../../hooks/useUrlParams'; +import { callApmApi } from '../../../../services/rest/createCallApmApi'; +import { unit } from '../../../../style/variables'; +import { asPercent } from '../../../../utils/formatters'; +// @ts-ignore +import CustomPlot from '../CustomPlot'; + +const tickFormatY = (y?: number) => { + return asPercent(y || 0, 1); +}; + +export const ErrorRateChart = () => { + const { urlParams, uiFilters } = useUrlParams(); + const syncedChartsProps = useChartsSync(); + + const { serviceName, start, end, errorGroupId } = urlParams; + const { data: errorRateData } = useFetcher(() => { + if (serviceName && start && end) { + return callApmApi({ + pathname: '/api/apm/services/{serviceName}/errors/rate', + params: { + path: { + serviceName, + }, + query: { + start, + end, + uiFilters: JSON.stringify(uiFilters), + groupId: errorGroupId, + }, + }, + }); + } + }, [serviceName, start, end, uiFilters, errorGroupId]); + + const combinedOnHover = useCallback( + (hoverX: number) => { + return syncedChartsProps.onHover(hoverX); + }, + [syncedChartsProps] + ); + + const errorRates = errorRateData?.errorRates || []; + + return ( + <> + + + {i18n.translate('xpack.apm.errorRateChart.title', { + defaultMessage: 'Error Rate', + })} + + + rate.y))), + legendClickDisabled: true, + title: i18n.translate('xpack.apm.errorRateChart.avgLabel', { + defaultMessage: 'Avg.', + }), + type: 'linemark', + hideTooltipValue: true, + }, + { + data: errorRates, + type: 'line', + color: theme.euiColorVis7, + hideLegend: true, + title: i18n.translate('xpack.apm.errorRateChart.rateLabel', { + defaultMessage: 'Rate', + }), + }, + ]} + onHover={combinedOnHover} + tickFormatY={tickFormatY} + formatTooltipValue={({ y }: { y?: number }) => + Number.isFinite(y) ? tickFormatY(y) : 'N/A' + } + height={unit * 10} + /> + + ); +}; diff --git a/x-pack/plugins/apm/public/components/shared/charts/Histogram/__test__/__snapshots__/Histogram.test.js.snap b/x-pack/plugins/apm/public/components/shared/charts/Histogram/__test__/__snapshots__/Histogram.test.js.snap index 1f935af7c899..a31b9735628a 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/Histogram/__test__/__snapshots__/Histogram.test.js.snap +++ b/x-pack/plugins/apm/public/components/shared/charts/Histogram/__test__/__snapshots__/Histogram.test.js.snap @@ -114,7 +114,7 @@ exports[`Histogram Initially should have default markup 1`] = ` x1={0} x2={0} y1={-0} - y2={10} + y2={0} /> 0 ms @@ -149,7 +149,7 @@ exports[`Histogram Initially should have default markup 1`] = ` x1={0} x2={0} y1={-0} - y2={10} + y2={0} /> 500 ms @@ -184,7 +184,7 @@ exports[`Histogram Initially should have default markup 1`] = ` x1={0} x2={0} y1={-0} - y2={10} + y2={0} /> 1,000 ms @@ -219,7 +219,7 @@ exports[`Histogram Initially should have default markup 1`] = ` x1={0} x2={0} y1={-0} - y2={10} + y2={0} /> 1,500 ms @@ -254,7 +254,7 @@ exports[`Histogram Initially should have default markup 1`] = ` x1={0} x2={0} y1={-0} - y2={10} + y2={0} /> 2,000 ms @@ -289,7 +289,7 @@ exports[`Histogram Initially should have default markup 1`] = ` x1={0} x2={0} y1={-0} - y2={10} + y2={0} /> 2,500 ms @@ -324,7 +324,7 @@ exports[`Histogram Initially should have default markup 1`] = ` x1={0} x2={0} y1={-0} - y2={10} + y2={0} /> 3,000 ms diff --git a/x-pack/plugins/apm/public/components/shared/charts/Histogram/index.js b/x-pack/plugins/apm/public/components/shared/charts/Histogram/index.js index 4eca1a37c51b..002ff19d0d1d 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/Histogram/index.js +++ b/x-pack/plugins/apm/public/components/shared/charts/Histogram/index.js @@ -26,6 +26,10 @@ import Tooltip from '../Tooltip'; import theme from '@elastic/eui/dist/eui_theme_light.json'; import { tint } from 'polished'; import { getTimeTicksTZ, getDomainTZ } from '../helper/timezone'; +import Legends from '../CustomPlot/Legends'; +import StatusText from '../CustomPlot/StatusText'; +import { i18n } from '@kbn/i18n'; +import { isValidCoordinateValue } from '../../../../utils/isValidCoordinateValue'; const XY_HEIGHT = unit * 10; const XY_MARGIN = { @@ -99,6 +103,7 @@ export class HistogramInner extends PureComponent { tooltipHeader, verticalLineHover, width: XY_WIDTH, + legends, } = this.props; const { hoveredBucket } = this.state; if (isEmpty(buckets) || XY_WIDTH === 0) { @@ -139,102 +144,140 @@ export class HistogramInner extends PureComponent { const showVerticalLineHover = verticalLineHover(hoveredBucket); const showBackgroundHover = backgroundHover(hoveredBucket); + const hasValidCoordinates = buckets.some((bucket) => + isValidCoordinateValue(bucket.y) + ); + const noHits = this.props.noHits || !hasValidCoordinates; + + const xyPlotProps = { + dontCheckIfEmpty: true, + xType: this.props.xType, + width: XY_WIDTH, + height: XY_HEIGHT, + margin: XY_MARGIN, + xDomain: xDomain, + yDomain: yDomain, + }; + + const xAxisProps = { + style: { strokeWidth: '1px' }, + marginRight: 10, + tickSize: 0, + tickTotal: X_TICK_TOTAL, + tickFormat: formatX, + tickValues: xTickValues, + }; + + const emptyStateChart = ( + + + + + ); + return (
- - - - - - {showBackgroundHover && ( - - )} - - {shouldShowTooltip && ( - - )} - - {selectedBucket && ( - - )} - - - - {showVerticalLineHover && ( - - )} - - { - return { - ...bucket, - xCenter: (bucket.x0 + bucket.x) / 2, - }; - })} - onClick={this.onClick} - onHover={this.onHover} - onBlur={this.onBlur} - x={(d) => x(d.xCenter)} - y={() => 1} - /> - + {noHits ? ( + <>{emptyStateChart} + ) : ( + <> + + + + + + {showBackgroundHover && ( + + )} + + {shouldShowTooltip && ( + + )} + + {selectedBucket && ( + + )} + + + + {showVerticalLineHover && hoveredBucket?.x && ( + + )} + + { + return { + ...bucket, + xCenter: (bucket.x0 + bucket.x) / 2, + }; + })} + onClick={this.onClick} + onHover={this.onHover} + onBlur={this.onBlur} + x={(d) => x(d.xCenter)} + y={() => 1} + /> + + + {legends && ( + {}} + truncateLegends={false} + noHits={noHits} + /> + )} + + )}
); @@ -255,6 +298,8 @@ HistogramInner.propTypes = { verticalLineHover: PropTypes.func, width: PropTypes.number.isRequired, xType: PropTypes.string, + legends: PropTypes.array, + noHits: PropTypes.bool, }; HistogramInner.defaultProps = { @@ -265,6 +310,7 @@ HistogramInner.defaultProps = { tooltipHeader: () => null, verticalLineHover: () => null, xType: 'linear', + noHits: false, }; export default makeWidthFlexible(HistogramInner); diff --git a/x-pack/plugins/apm/server/lib/errors/get_error_rate.ts b/x-pack/plugins/apm/server/lib/errors/get_error_rate.ts new file mode 100644 index 000000000000..d558e3942a42 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/errors/get_error_rate.ts @@ -0,0 +1,109 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { + ERROR_GROUP_ID, + PROCESSOR_EVENT, + SERVICE_NAME, +} from '../../../common/elasticsearch_fieldnames'; +import { ProcessorEvent } from '../../../common/processor_event'; +import { getMetricsDateHistogramParams } from '../helpers/metrics'; +import { rangeFilter } from '../helpers/range_filter'; +import { + Setup, + SetupTimeRange, + SetupUIFilters, +} from '../helpers/setup_request'; + +export async function getErrorRate({ + serviceName, + groupId, + setup, +}: { + serviceName: string; + groupId?: string; + setup: Setup & SetupTimeRange & SetupUIFilters; +}) { + const { start, end, uiFiltersES, client, indices } = setup; + + const filter = [ + { term: { [SERVICE_NAME]: serviceName } }, + { range: rangeFilter(start, end) }, + ...uiFiltersES, + ]; + + const aggs = { + response_times: { + date_histogram: getMetricsDateHistogramParams(start, end), + }, + }; + + const getTransactionBucketAggregation = async () => { + const resp = await client.search({ + index: indices['apm_oss.transactionIndices'], + body: { + size: 0, + query: { + bool: { + filter: [ + ...filter, + { term: { [PROCESSOR_EVENT]: ProcessorEvent.transaction } }, + ], + }, + }, + aggs, + }, + }); + return { + totalHits: resp.hits.total.value, + responseTimeBuckets: resp.aggregations?.response_times.buckets, + }; + }; + const getErrorBucketAggregation = async () => { + const groupIdFilter = groupId + ? [{ term: { [ERROR_GROUP_ID]: groupId } }] + : []; + const resp = await client.search({ + index: indices['apm_oss.errorIndices'], + body: { + size: 0, + query: { + bool: { + filter: [ + ...filter, + ...groupIdFilter, + { term: { [PROCESSOR_EVENT]: ProcessorEvent.error } }, + ], + }, + }, + aggs, + }, + }); + return resp.aggregations?.response_times.buckets; + }; + + const [transactions, errorResponseTimeBuckets] = await Promise.all([ + getTransactionBucketAggregation(), + getErrorBucketAggregation(), + ]); + + const transactionCountByTimestamp: Record = {}; + if (transactions?.responseTimeBuckets) { + transactions.responseTimeBuckets.forEach((bucket) => { + transactionCountByTimestamp[bucket.key] = bucket.doc_count; + }); + } + + const errorRates = errorResponseTimeBuckets?.map((bucket) => { + const { key, doc_count: errorCount } = bucket; + const relativeRate = errorCount / transactionCountByTimestamp[key]; + return { x: key, y: relativeRate }; + }); + + return { + noHits: transactions?.totalHits === 0, + errorRates, + }; +} diff --git a/x-pack/plugins/apm/server/routes/create_apm_api.ts b/x-pack/plugins/apm/server/routes/create_apm_api.ts index 774f1f27435a..bdfb49fa3082 100644 --- a/x-pack/plugins/apm/server/routes/create_apm_api.ts +++ b/x-pack/plugins/apm/server/routes/create_apm_api.ts @@ -13,6 +13,7 @@ import { errorDistributionRoute, errorGroupsRoute, errorsRoute, + errorRateRoute, } from './errors'; import { serviceAgentNameRoute, @@ -81,6 +82,7 @@ const createApmApi = () => { .add(errorDistributionRoute) .add(errorGroupsRoute) .add(errorsRoute) + .add(errorRateRoute) // Services .add(serviceAgentNameRoute) diff --git a/x-pack/plugins/apm/server/routes/errors.ts b/x-pack/plugins/apm/server/routes/errors.ts index 1615550027d3..97314a9a6166 100644 --- a/x-pack/plugins/apm/server/routes/errors.ts +++ b/x-pack/plugins/apm/server/routes/errors.ts @@ -11,6 +11,7 @@ import { getErrorGroup } from '../lib/errors/get_error_group'; import { getErrorGroups } from '../lib/errors/get_error_groups'; import { setupRequest } from '../lib/helpers/setup_request'; import { uiFiltersRt, rangeRt } from './default_api_types'; +import { getErrorRate } from '../lib/errors/get_error_rate'; export const errorsRoute = createRoute(() => ({ path: '/api/apm/services/{serviceName}/errors', @@ -80,3 +81,26 @@ export const errorDistributionRoute = createRoute(() => ({ return getErrorDistribution({ serviceName, groupId, setup }); }, })); + +export const errorRateRoute = createRoute(() => ({ + path: '/api/apm/services/{serviceName}/errors/rate', + params: { + path: t.type({ + serviceName: t.string, + }), + query: t.intersection([ + t.partial({ + groupId: t.string, + }), + uiFiltersRt, + rangeRt, + ]), + }, + handler: async ({ context, request }) => { + const setup = await setupRequest(context, request); + const { params } = context; + const { serviceName } = params.path; + const { groupId } = params.query; + return getErrorRate({ serviceName, groupId, setup }); + }, +})); diff --git a/x-pack/plugins/beats_management/server/index.ts b/x-pack/plugins/beats_management/server/index.ts index 607fb0ab2725..ad19087f5ac9 100644 --- a/x-pack/plugins/beats_management/server/index.ts +++ b/x-pack/plugins/beats_management/server/index.ts @@ -4,7 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ +import { PluginInitializer } from '../../../../src/core/server'; import { beatsManagementConfigSchema } from '../common'; +import { BeatsManagementPlugin } from './plugin'; export const config = { schema: beatsManagementConfigSchema, @@ -16,8 +18,4 @@ export const config = { }, }; -export const plugin = () => ({ - setup() {}, - start() {}, - stop() {}, -}); +export const plugin: PluginInitializer<{}, {}> = (context) => new BeatsManagementPlugin(context); diff --git a/x-pack/plugins/beats_management/server/plugin.ts b/x-pack/plugins/beats_management/server/plugin.ts new file mode 100644 index 000000000000..a82dbcb4a3a6 --- /dev/null +++ b/x-pack/plugins/beats_management/server/plugin.ts @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + CoreSetup, + CoreStart, + Plugin, + PluginInitializerContext, +} from '../../../../src/core/server'; +import { SecurityPluginSetup } from '../../security/server'; +import { LicensingPluginStart } from '../../licensing/server'; +import { BeatsManagementConfigType } from '../common'; + +interface SetupDeps { + security?: SecurityPluginSetup; +} + +interface StartDeps { + licensing: LicensingPluginStart; +} + +export class BeatsManagementPlugin implements Plugin<{}, {}, SetupDeps, StartDeps> { + constructor( + private readonly initializerContext: PluginInitializerContext + ) {} + + public async setup(core: CoreSetup, plugins: SetupDeps) { + this.initializerContext.config.create(); + + return {}; + } + + public async start(core: CoreStart, { licensing }: StartDeps) { + return {}; + } +} diff --git a/x-pack/legacy/plugins/beats_management/types/formsy.d.ts b/x-pack/plugins/beats_management/types/formsy.d.ts similarity index 100% rename from x-pack/legacy/plugins/beats_management/types/formsy.d.ts rename to x-pack/plugins/beats_management/types/formsy.d.ts diff --git a/x-pack/plugins/case/server/routes/api/utils.test.ts b/x-pack/plugins/case/server/routes/api/utils.test.ts index 81156b98bab8..2da489e64343 100644 --- a/x-pack/plugins/case/server/routes/api/utils.test.ts +++ b/x-pack/plugins/case/server/routes/api/utils.test.ts @@ -222,7 +222,12 @@ describe('Utils', () => { ]; const res = transformCases( - { saved_objects: mockCases, total: mockCases.length, per_page: 10, page: 1 }, + { + saved_objects: mockCases.map((obj) => ({ ...obj, score: 1 })), + total: mockCases.length, + per_page: 10, + page: 1, + }, 2, 2, extraCaseData, @@ -232,7 +237,11 @@ describe('Utils', () => { page: 1, per_page: 10, total: mockCases.length, - cases: flattenCaseSavedObjects(mockCases, extraCaseData, '123'), + cases: flattenCaseSavedObjects( + mockCases.map((obj) => ({ ...obj, score: 1 })), + extraCaseData, + '123' + ), count_open_cases: 2, count_closed_cases: 2, }); @@ -500,7 +509,7 @@ describe('Utils', () => { describe('transformComments', () => { it('transforms correctly', () => { const comments = { - saved_objects: mockCaseComments, + saved_objects: mockCaseComments.map((obj) => ({ ...obj, score: 1 })), total: mockCaseComments.length, per_page: 10, page: 1, diff --git a/x-pack/plugins/case/server/routes/api/utils.ts b/x-pack/plugins/case/server/routes/api/utils.ts index b7f3c68d1662..ec2881807442 100644 --- a/x-pack/plugins/case/server/routes/api/utils.ts +++ b/x-pack/plugins/case/server/routes/api/utils.ts @@ -101,7 +101,7 @@ export const transformCases = ( }); export const flattenCaseSavedObjects = ( - savedObjects: SavedObjectsFindResponse['saved_objects'], + savedObjects: Array>, totalCommentByCase: TotalCommentByCase[], caseConfigureConnectorId: string = 'none' ): CaseResponse[] => @@ -146,7 +146,7 @@ export const transformComments = ( }); export const flattenCommentSavedObjects = ( - savedObjects: SavedObjectsFindResponse['saved_objects'] + savedObjects: Array> ): CommentResponse[] => savedObjects.reduce((acc: CommentResponse[], savedObject: SavedObject) => { return [...acc, flattenCommentSavedObject(savedObject)]; diff --git a/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.test.ts b/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.test.ts index 7098f611defa..ec5d81532e23 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.test.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.test.ts @@ -676,12 +676,14 @@ describe('#find', () => { id: 'some-id', type: 'unknown-type', attributes: { attrOne: 'one', attrSecret: 'secret', attrThree: 'three' }, + score: 1, references: [], }, { id: 'some-id-2', type: 'unknown-type', attributes: { attrOne: 'one', attrSecret: 'secret', attrThree: 'three' }, + score: 1, references: [], }, ], @@ -722,6 +724,7 @@ describe('#find', () => { attrNotSoSecret: 'not-so-secret', attrThree: 'three', }, + score: 1, references: [], }, { @@ -733,6 +736,7 @@ describe('#find', () => { attrNotSoSecret: '*not-so-secret*', attrThree: 'three', }, + score: 1, references: [], }, ], @@ -793,6 +797,7 @@ describe('#find', () => { attrNotSoSecret: 'not-so-secret', attrThree: 'three', }, + score: 1, references: [], }, { @@ -804,6 +809,7 @@ describe('#find', () => { attrNotSoSecret: '*not-so-secret*', attrThree: 'three', }, + score: 1, references: [], }, ], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts index fe66496f70dc..9928ce4807da 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts @@ -549,6 +549,7 @@ export const getFindResultStatus = (): SavedObjectsFindResponse< searchAfterTimeDurations: ['200.00'], bulkCreateTimeDurations: ['800.43'], }, + score: 1, references: [], updated_at: '2020-02-18T15:26:51.333Z', version: 'WzQ2LDFd', @@ -570,6 +571,7 @@ export const getFindResultStatus = (): SavedObjectsFindResponse< searchAfterTimeDurations: ['200.00'], bulkCreateTimeDurations: ['800.43'], }, + score: 1, references: [], updated_at: '2020-02-18T15:15:58.860Z', version: 'WzMyLDFd', diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts index 6056e692854a..01ee41e3b877 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts @@ -391,7 +391,7 @@ export const exampleFindRuleStatusResponse: ( total: 1, per_page: 6, page: 1, - saved_objects: mockStatuses, + saved_objects: mockStatuses.map((obj) => ({ ...obj, score: 1 })), }); export const mockLogger: Logger = loggingServiceMock.createLogger(); diff --git a/x-pack/plugins/spaces/server/saved_objects/spaces_saved_objects_client.test.ts b/x-pack/plugins/spaces/server/saved_objects/spaces_saved_objects_client.test.ts index 75cd501a1a9a..190429d2dacd 100644 --- a/x-pack/plugins/spaces/server/saved_objects/spaces_saved_objects_client.test.ts +++ b/x-pack/plugins/spaces/server/saved_objects/spaces_saved_objects_client.test.ts @@ -138,7 +138,7 @@ const ERROR_NAMESPACE_SPECIFIED = 'Spaces currently determines the namespaces'; test(`passes options.type to baseClient if valid singular type specified`, async () => { const { client, baseClient } = await createSpacesSavedObjectsClient(); const expectedReturnValue = { - saved_objects: [createMockResponse()], + saved_objects: [createMockResponse()].map((obj) => ({ ...obj, score: 1 })), total: 1, per_page: 0, page: 0, @@ -158,7 +158,7 @@ const ERROR_NAMESPACE_SPECIFIED = 'Spaces currently determines the namespaces'; test(`supplements options with the current namespace`, async () => { const { client, baseClient } = await createSpacesSavedObjectsClient(); const expectedReturnValue = { - saved_objects: [createMockResponse()], + saved_objects: [createMockResponse()].map((obj) => ({ ...obj, score: 1 })), total: 1, per_page: 0, page: 0, diff --git a/x-pack/scripts/functional_tests.js b/x-pack/scripts/functional_tests.js index 37b22a687741..6cafa3eeef08 100644 --- a/x-pack/scripts/functional_tests.js +++ b/x-pack/scripts/functional_tests.js @@ -51,6 +51,7 @@ const onlyNotInCoverageTests = [ require.resolve('../test/licensing_plugin/config.legacy.ts'), require.resolve('../test/endpoint_api_integration_no_ingest/config.ts'), require.resolve('../test/reporting_api_integration/config.js'), + require.resolve('../test/functional_embedded/config.ts'), ]; require('@kbn/plugin-helpers').babelRegister(); diff --git a/x-pack/test/functional_embedded/config.firefox.ts b/x-pack/test/functional_embedded/config.firefox.ts new file mode 100644 index 000000000000..2051d1afd4ab --- /dev/null +++ b/x-pack/test/functional_embedded/config.firefox.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const chromeConfig = await readConfigFile(require.resolve('./config')); + + return { + ...chromeConfig.getAll(), + + browser: { + type: 'firefox', + acceptInsecureCerts: true, + }, + + suiteTags: { + exclude: ['skipFirefox'], + }, + + junit: { + reportName: 'Firefox Kibana Embedded in iframe with X-Pack Security', + }, + }; +} diff --git a/x-pack/test/functional_embedded/config.ts b/x-pack/test/functional_embedded/config.ts new file mode 100644 index 000000000000..95b290ece7db --- /dev/null +++ b/x-pack/test/functional_embedded/config.ts @@ -0,0 +1,67 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import Fs from 'fs'; +import { resolve } from 'path'; +import { CA_CERT_PATH, KBN_CERT_PATH, KBN_KEY_PATH } from '@kbn/dev-utils'; +import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; +import { pageObjects } from '../functional/page_objects'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const kibanaFunctionalConfig = await readConfigFile(require.resolve('../functional/config.js')); + + const iframeEmbeddedPlugin = resolve(__dirname, './plugins/iframe_embedded'); + + const servers = { + ...kibanaFunctionalConfig.get('servers'), + elasticsearch: { + ...kibanaFunctionalConfig.get('servers.elasticsearch'), + }, + kibana: { + ...kibanaFunctionalConfig.get('servers.kibana'), + protocol: 'https', + ssl: { + enabled: true, + key: Fs.readFileSync(KBN_KEY_PATH).toString('utf8'), + certificate: Fs.readFileSync(KBN_CERT_PATH).toString('utf8'), + certificateAuthorities: Fs.readFileSync(CA_CERT_PATH).toString('utf8'), + }, + }, + }; + + return { + testFiles: [require.resolve('./tests')], + servers, + services: kibanaFunctionalConfig.get('services'), + pageObjects, + browser: { + acceptInsecureCerts: true, + }, + junit: { + reportName: 'Kibana Embedded in iframe with X-Pack Security', + }, + + esTestCluster: kibanaFunctionalConfig.get('esTestCluster'), + apps: { + ...kibanaFunctionalConfig.get('apps'), + }, + + kbnTestServer: { + ...kibanaFunctionalConfig.get('kbnTestServer'), + serverArgs: [ + ...kibanaFunctionalConfig.get('kbnTestServer.serverArgs'), + `--plugin-path=${iframeEmbeddedPlugin}`, + '--server.ssl.enabled=true', + `--server.ssl.key=${KBN_KEY_PATH}`, + `--server.ssl.certificate=${KBN_CERT_PATH}`, + `--server.ssl.certificateAuthorities=${CA_CERT_PATH}`, + + '--xpack.security.sameSiteCookies=None', + '--xpack.security.secureCookies=true', + ], + }, + }; +} diff --git a/x-pack/test/functional_embedded/ftr_provider_context.d.ts b/x-pack/test/functional_embedded/ftr_provider_context.d.ts new file mode 100644 index 000000000000..5646c06a3cd3 --- /dev/null +++ b/x-pack/test/functional_embedded/ftr_provider_context.d.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { GenericFtrProviderContext } from '@kbn/test/types/ftr'; +import { pageObjects } from '../functional/page_objects'; +import { services } from './services'; + +export type FtrProviderContext = GenericFtrProviderContext; +export { pageObjects }; diff --git a/x-pack/test/functional_embedded/plugins/iframe_embedded/kibana.json b/x-pack/test/functional_embedded/plugins/iframe_embedded/kibana.json new file mode 100644 index 000000000000..ea9f55bd21c6 --- /dev/null +++ b/x-pack/test/functional_embedded/plugins/iframe_embedded/kibana.json @@ -0,0 +1,7 @@ +{ + "id": "iframe_embedded", + "version": "1.0.0", + "kibanaVersion": "kibana", + "server": true, + "ui": false +} diff --git a/x-pack/test/functional_embedded/plugins/iframe_embedded/package.json b/x-pack/test/functional_embedded/plugins/iframe_embedded/package.json new file mode 100644 index 000000000000..9fa1554e5312 --- /dev/null +++ b/x-pack/test/functional_embedded/plugins/iframe_embedded/package.json @@ -0,0 +1,14 @@ +{ + "name": "iframe_embedded", + "version": "0.0.0", + "kibana": { + "version": "kibana" + }, + "scripts": { + "kbn": "node ../../../../../scripts/kbn.js", + "build": "rm -rf './target' && tsc" + }, + "devDependencies": { + "typescript": "3.9.5" + } +} diff --git a/x-pack/test/functional_embedded/plugins/iframe_embedded/server/index.ts b/x-pack/test/functional_embedded/plugins/iframe_embedded/server/index.ts new file mode 100644 index 000000000000..976ef19d4d8a --- /dev/null +++ b/x-pack/test/functional_embedded/plugins/iframe_embedded/server/index.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { PluginInitializerContext } from 'kibana/server'; +import { IframeEmbeddedPlugin } from './plugin'; + +export const plugin = (initContext: PluginInitializerContext) => + new IframeEmbeddedPlugin(initContext); diff --git a/x-pack/test/functional_embedded/plugins/iframe_embedded/server/plugin.ts b/x-pack/test/functional_embedded/plugins/iframe_embedded/server/plugin.ts new file mode 100644 index 000000000000..890fe14cf03c --- /dev/null +++ b/x-pack/test/functional_embedded/plugins/iframe_embedded/server/plugin.ts @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import Url from 'url'; +import { Plugin, CoreSetup, PluginInitializerContext } from 'src/core/server'; + +function renderBody(iframeUrl: string) { + return ` + + + + + Kibana embedded in iframe + + +