mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[Profiling] creating API tests (#159984)
As part of the actions for making Profiling production ready, this PR adds basic API tests on the Profiling APIs checking if only users with `access:profiling` are allowed to call our APIs, other users must be forbidden. --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
eaa9eb4b1f
commit
4ddb96f9e3
20 changed files with 34144 additions and 2 deletions
|
@ -399,3 +399,4 @@ enabled:
|
|||
- x-pack/performance/journeys/dashboard_listing_page.ts
|
||||
- x-pack/performance/journeys/cloud_security_dashboard.ts
|
||||
- x-pack/test/custom_branding/config.ts
|
||||
- x-pack/test/profiling_api_integration/cloud/config.ts
|
||||
|
|
|
@ -604,6 +604,7 @@ module.exports = {
|
|||
'**/cypress.config.{js,ts}',
|
||||
'x-pack/test_serverless/**/config*.ts',
|
||||
'x-pack/test_serverless/*/test_suites/**/*',
|
||||
'x-pack/test/profiling_api_integration/**/*.ts',
|
||||
],
|
||||
rules: {
|
||||
'import/no-default-export': 'off',
|
||||
|
|
3
.github/CODEOWNERS
vendored
3
.github/CODEOWNERS
vendored
|
@ -1262,6 +1262,9 @@ x-pack/test/threat_intelligence_cypress @elastic/protections-experience
|
|||
# Changes to translation files should not ping code reviewers
|
||||
x-pack/plugins/translations/translations
|
||||
|
||||
# Profiling api integration testing
|
||||
x-pack/test/profiling_api_integration @elastic/profiling-ui
|
||||
|
||||
####
|
||||
## These rules are always last so they take ultimate priority over everything else
|
||||
####
|
||||
|
|
|
@ -28,7 +28,6 @@ export function getRoutePaths() {
|
|||
TopNTraces: `${BASE_ROUTE_PATH}/topn/traces`,
|
||||
Flamechart: `${BASE_ROUTE_PATH}/flamechart`,
|
||||
HasSetupESResources: `${BASE_ROUTE_PATH}/setup/es_resources`,
|
||||
HasSetupDataCollection: `${BASE_ROUTE_PATH}/setup/has_data`,
|
||||
SetupDataCollectionInstructions: `${BASE_ROUTE_PATH}/setup/instructions`,
|
||||
};
|
||||
}
|
||||
|
|
111
x-pack/plugins/profiling/scripts/test/api.js
Normal file
111
x-pack/plugins/profiling/scripts/test/api.js
Normal file
|
@ -0,0 +1,111 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
/* eslint-disable no-console */
|
||||
const { times } = require('lodash');
|
||||
const yargs = require('yargs');
|
||||
const path = require('path');
|
||||
const childProcess = require('child_process');
|
||||
|
||||
const { argv } = yargs(process.argv.slice(2))
|
||||
.option('server', {
|
||||
default: false,
|
||||
type: 'boolean',
|
||||
description: 'Only start ES and Kibana',
|
||||
})
|
||||
.option('runner', {
|
||||
default: false,
|
||||
type: 'boolean',
|
||||
description: 'Only run tests',
|
||||
})
|
||||
.option('grep', {
|
||||
alias: 'spec',
|
||||
type: 'string',
|
||||
description: 'Specify the specs to run',
|
||||
})
|
||||
.option('grep-files', {
|
||||
alias: 'files',
|
||||
type: 'array',
|
||||
string: true,
|
||||
description: 'Specify the files to run',
|
||||
})
|
||||
.option('inspect', {
|
||||
default: false,
|
||||
type: 'boolean',
|
||||
description: 'Add --inspect-brk flag to the ftr for debugging',
|
||||
})
|
||||
.option('times', {
|
||||
type: 'number',
|
||||
description: 'Repeat the test n number of times',
|
||||
})
|
||||
.option('updateSnapshots', {
|
||||
default: false,
|
||||
type: 'boolean',
|
||||
description: 'Update snapshots',
|
||||
})
|
||||
.check((argv) => {
|
||||
const { inspect, runner } = argv;
|
||||
if (inspect && !runner) {
|
||||
throw new Error('--inspect can only be used with --runner');
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
})
|
||||
.help();
|
||||
|
||||
const { server, runner, grep, grepFiles, inspect, updateSnapshots } = argv;
|
||||
|
||||
const license = 'cloud';
|
||||
|
||||
console.log(`License: ${license}`);
|
||||
|
||||
let ftrScript = 'functional_tests';
|
||||
if (server) {
|
||||
ftrScript = 'functional_tests_server';
|
||||
} else if (runner) {
|
||||
ftrScript = 'functional_test_runner';
|
||||
}
|
||||
|
||||
const cmd = [
|
||||
'node',
|
||||
...(inspect ? ['--inspect-brk'] : []),
|
||||
`../../../../../scripts/${ftrScript}`,
|
||||
...(grep ? [`--grep "${grep}"`] : []),
|
||||
...(updateSnapshots ? [`--updateSnapshots`] : []),
|
||||
`--config ../../../../test/profiling_api_integration/${license}/config.ts`,
|
||||
].join(' ');
|
||||
|
||||
console.log(`Running: "${cmd}"`);
|
||||
|
||||
function runTests() {
|
||||
childProcess.execSync(cmd, {
|
||||
cwd: path.join(__dirname),
|
||||
stdio: 'inherit',
|
||||
env: { ...process.env, PROFILING_TEST_GREP_FILES: JSON.stringify(grepFiles) },
|
||||
});
|
||||
}
|
||||
|
||||
if (argv.times) {
|
||||
const runCounter = { succeeded: 0, failed: 0, remaining: argv.times };
|
||||
let exitStatus = 0;
|
||||
times(argv.times, () => {
|
||||
try {
|
||||
runTests();
|
||||
runCounter.succeeded++;
|
||||
} catch (e) {
|
||||
exitStatus = 1;
|
||||
runCounter.failed++;
|
||||
}
|
||||
runCounter.remaining--;
|
||||
if (argv.times > 1) {
|
||||
console.log(runCounter);
|
||||
}
|
||||
});
|
||||
process.exit(exitStatus);
|
||||
} else {
|
||||
runTests();
|
||||
}
|
10
x-pack/test/profiling_api_integration/cloud/config.ts
Normal file
10
x-pack/test/profiling_api_integration/cloud/config.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { configs } from '../configs';
|
||||
|
||||
export default configs.cloud;
|
|
@ -0,0 +1,74 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { formatRequest } from '@kbn/server-route-repository';
|
||||
import request from 'superagent';
|
||||
import supertest from 'supertest';
|
||||
import { format } from 'url';
|
||||
|
||||
export function createProfilingApiClient(st: supertest.SuperTest<supertest.Test>) {
|
||||
return async (options: {
|
||||
endpoint: string;
|
||||
params?: {
|
||||
query?: any;
|
||||
path?: any;
|
||||
};
|
||||
}) => {
|
||||
const { endpoint } = options;
|
||||
|
||||
const params = 'params' in options ? (options.params as Record<string, any>) : {};
|
||||
|
||||
const { method, pathname, version } = formatRequest(endpoint, params.path);
|
||||
const url = format({ pathname, query: params?.query });
|
||||
|
||||
const headers: Record<string, string> = { 'kbn-xsrf': 'foo' };
|
||||
|
||||
if (version) {
|
||||
headers['Elastic-Api-Version'] = version;
|
||||
}
|
||||
|
||||
let res: request.Response;
|
||||
if (params.body) {
|
||||
res = await st[method](url).send(params.body).set(headers);
|
||||
} else {
|
||||
res = await st[method](url).set(headers);
|
||||
}
|
||||
|
||||
// supertest doesn't throw on http errors
|
||||
if (res?.status !== 200 && res?.status !== 202) {
|
||||
throw new ProfilingApiError(res, endpoint);
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
}
|
||||
|
||||
type ApiErrorResponse = Omit<request.Response, 'body'> & {
|
||||
body: {
|
||||
statusCode: number;
|
||||
error: string;
|
||||
message: string;
|
||||
attributes: object;
|
||||
};
|
||||
};
|
||||
|
||||
export type ProfilingApiSupertest = ReturnType<typeof createProfilingApiClient>;
|
||||
|
||||
export class ProfilingApiError extends Error {
|
||||
res: ApiErrorResponse;
|
||||
|
||||
constructor(res: request.Response, endpoint: string) {
|
||||
super(
|
||||
`Unhandled ProfilingApiError.
|
||||
Status: "${res.status}"
|
||||
Endpoint: "${endpoint}"
|
||||
Body: ${JSON.stringify(res.body)}`
|
||||
);
|
||||
|
||||
this.res = res;
|
||||
}
|
||||
}
|
163
x-pack/test/profiling_api_integration/common/config.ts
Normal file
163
x-pack/test/profiling_api_integration/common/config.ts
Normal file
|
@ -0,0 +1,163 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { format, UrlObject } from 'url';
|
||||
import { FtrConfigProviderContext } from '@kbn/test';
|
||||
import supertest from 'supertest';
|
||||
import { getRoutePaths } from '@kbn/profiling-plugin/common';
|
||||
import { ProfilingFtrConfigName } from '../configs';
|
||||
import {
|
||||
FtrProviderContext,
|
||||
InheritedFtrProviderContext,
|
||||
InheritedServices,
|
||||
} from './ftr_provider_context';
|
||||
import { RegistryProvider } from './registry';
|
||||
import { createProfilingApiClient } from './api_supertest';
|
||||
import {
|
||||
ProfilingUsername,
|
||||
PROFILING_TEST_PASSWORD,
|
||||
} from './create_profiling_users/authentication';
|
||||
import { createProfilingUsers } from './create_profiling_users';
|
||||
|
||||
export type CreateTestConfig = ReturnType<typeof createTestConfig>;
|
||||
const profilingRoutePaths = getRoutePaths();
|
||||
|
||||
export async function getProfilingApiClient({
|
||||
kibanaServer,
|
||||
username,
|
||||
}: {
|
||||
kibanaServer: UrlObject;
|
||||
username: ProfilingUsername | 'elastic';
|
||||
}) {
|
||||
const url = format({
|
||||
...kibanaServer,
|
||||
auth: `${username}:${PROFILING_TEST_PASSWORD}`,
|
||||
});
|
||||
|
||||
return createProfilingApiClient(supertest(url));
|
||||
}
|
||||
|
||||
type ProfilingApiClientKey = 'noAccessUser' | 'readUser' | 'adminUser';
|
||||
|
||||
export type ProfilingApiClient = Record<
|
||||
ProfilingApiClientKey,
|
||||
Awaited<ReturnType<typeof getProfilingApiClient>>
|
||||
>;
|
||||
|
||||
export interface ProfilingFtrConfig {
|
||||
name: ProfilingFtrConfigName;
|
||||
license: 'basic' | 'trial';
|
||||
kibanaConfig?: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface CreateTest {
|
||||
testFiles: string[];
|
||||
servers: any;
|
||||
servicesRequiredForTestAnalysis: string[];
|
||||
services: InheritedServices & {
|
||||
profilingFtrConfig: () => ProfilingFtrConfig;
|
||||
registry: ({ getService }: FtrProviderContext) => ReturnType<typeof RegistryProvider>;
|
||||
profilingApiClient: (context: InheritedFtrProviderContext) => ProfilingApiClient;
|
||||
};
|
||||
junit: { reportName: string };
|
||||
esTestCluster: any;
|
||||
kbnTestServer: any;
|
||||
}
|
||||
|
||||
export function createTestConfig(
|
||||
config: ProfilingFtrConfig
|
||||
): ({ readConfigFile }: FtrConfigProviderContext) => Promise<CreateTest> {
|
||||
const { license, name, kibanaConfig } = config;
|
||||
|
||||
return async ({ readConfigFile }: FtrConfigProviderContext) => {
|
||||
const xPackAPITestsConfig = await readConfigFile(
|
||||
require.resolve('../../api_integration/config.ts')
|
||||
);
|
||||
|
||||
const services = xPackAPITestsConfig.get('services');
|
||||
const servers = xPackAPITestsConfig.get('servers');
|
||||
const kibanaServer = servers.kibana as UrlObject;
|
||||
const kibanaServerUrl = format(kibanaServer);
|
||||
const esServer = servers.elasticsearch as UrlObject;
|
||||
|
||||
return {
|
||||
testFiles: [require.resolve('../tests')],
|
||||
servers,
|
||||
servicesRequiredForTestAnalysis: ['profilingFtrConfig', 'registry'],
|
||||
services: {
|
||||
...services,
|
||||
profilingFtrConfig: () => config,
|
||||
registry: RegistryProvider,
|
||||
profilingApiClient: async (context: InheritedFtrProviderContext) => {
|
||||
const security = context.getService('security');
|
||||
const securityService = await security.init();
|
||||
|
||||
const { username, password } = servers.kibana;
|
||||
const esUrl = format(esServer);
|
||||
|
||||
await createProfilingUsers({
|
||||
securityService,
|
||||
elasticsearch: { node: esUrl, username, password },
|
||||
kibana: { hostname: kibanaServerUrl },
|
||||
});
|
||||
|
||||
const adminUser = await getProfilingApiClient({
|
||||
kibanaServer,
|
||||
username: 'elastic',
|
||||
});
|
||||
|
||||
await supertest(kibanaServerUrl).post('/api/fleet/setup').set('kbn-xsrf', 'foo');
|
||||
|
||||
const result = await adminUser({
|
||||
endpoint: `GET ${profilingRoutePaths.HasSetupESResources}`,
|
||||
});
|
||||
if (!result.body.has_setup) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Setting up Universal Profiling');
|
||||
await adminUser({
|
||||
endpoint: `POST ${profilingRoutePaths.HasSetupESResources}`,
|
||||
});
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Universal Profiling set up');
|
||||
}
|
||||
|
||||
return {
|
||||
noAccessUser: await getProfilingApiClient({
|
||||
kibanaServer,
|
||||
username: ProfilingUsername.noAccessUser,
|
||||
}),
|
||||
readUser: await getProfilingApiClient({
|
||||
kibanaServer,
|
||||
username: ProfilingUsername.viewerUser,
|
||||
}),
|
||||
adminUser,
|
||||
};
|
||||
},
|
||||
},
|
||||
junit: {
|
||||
reportName: `Profiling API Integration tests (${name})`,
|
||||
},
|
||||
esTestCluster: {
|
||||
...xPackAPITestsConfig.get('esTestCluster'),
|
||||
license,
|
||||
},
|
||||
kbnTestServer: {
|
||||
...xPackAPITestsConfig.get('kbnTestServer'),
|
||||
serverArgs: [
|
||||
...xPackAPITestsConfig.get('kbnTestServer.serverArgs'),
|
||||
...(kibanaConfig
|
||||
? Object.entries(kibanaConfig).map(([key, value]) =>
|
||||
Array.isArray(value) ? `--${key}=${JSON.stringify(value)}` : `--${key}=${value}`
|
||||
)
|
||||
: []),
|
||||
],
|
||||
},
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export type ProfilingServices = Awaited<ReturnType<CreateTestConfig>>['services'];
|
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export enum ProfilingUsername {
|
||||
noAccessUser = 'no_access_user',
|
||||
viewerUser = 'viewer',
|
||||
editorUser = 'editor',
|
||||
}
|
||||
|
||||
export const PROFILING_TEST_PASSWORD = 'changeme';
|
||||
|
||||
export const profilingUsers: Record<ProfilingUsername, { builtInRoleNames?: string[] }> = {
|
||||
[ProfilingUsername.noAccessUser]: {},
|
||||
[ProfilingUsername.viewerUser]: {
|
||||
builtInRoleNames: ['viewer'],
|
||||
},
|
||||
[ProfilingUsername.editorUser]: {
|
||||
builtInRoleNames: ['editor'],
|
||||
},
|
||||
};
|
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import axios, { AxiosRequestConfig, AxiosError } from 'axios';
|
||||
import { once } from 'lodash';
|
||||
import { Elasticsearch, Kibana } from '..';
|
||||
|
||||
export async function callKibana<T>({
|
||||
elasticsearch,
|
||||
kibana,
|
||||
options,
|
||||
}: {
|
||||
elasticsearch: Omit<Elasticsearch, 'node'>;
|
||||
kibana: Kibana;
|
||||
options: AxiosRequestConfig;
|
||||
}): Promise<T> {
|
||||
const baseUrl = await getBaseUrl(kibana.hostname);
|
||||
const { username, password } = elasticsearch;
|
||||
|
||||
const { data } = await axios.request({
|
||||
...options,
|
||||
baseURL: baseUrl,
|
||||
auth: { username, password },
|
||||
headers: { 'kbn-xsrf': 'true', ...options.headers },
|
||||
});
|
||||
return data;
|
||||
}
|
||||
|
||||
const getBaseUrl = once(async (kibanaHostname: string) => {
|
||||
try {
|
||||
await axios.request({ url: kibanaHostname, maxRedirects: 0 });
|
||||
} catch (e) {
|
||||
if (isAxiosError(e)) {
|
||||
const location = e.response?.headers?.location ?? '';
|
||||
const hasBasePath = RegExp(/^\/\w{3}$/).test(location);
|
||||
const basePath = hasBasePath ? location : '';
|
||||
return `${kibanaHostname}${basePath}`;
|
||||
}
|
||||
|
||||
throw e;
|
||||
}
|
||||
return kibanaHostname;
|
||||
});
|
||||
|
||||
export function isAxiosError(e: AxiosError | Error): e is AxiosError {
|
||||
return 'isAxiosError' in e;
|
||||
}
|
||||
|
||||
export class AbortError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,114 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
/* eslint-disable no-console */
|
||||
|
||||
import { difference, union } from 'lodash';
|
||||
import { Elasticsearch, Kibana } from '..';
|
||||
import { callKibana, isAxiosError } from './call_kibana';
|
||||
import { SecurityService } from '../../../../../../test/common/services/security/security';
|
||||
|
||||
interface User {
|
||||
username: string;
|
||||
roles: string[];
|
||||
full_name?: string;
|
||||
email?: string;
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
export async function createOrUpdateUser({
|
||||
elasticsearch,
|
||||
kibana,
|
||||
user,
|
||||
securityService,
|
||||
}: {
|
||||
elasticsearch: Elasticsearch;
|
||||
kibana: Kibana;
|
||||
user: User;
|
||||
securityService: SecurityService;
|
||||
}) {
|
||||
const existingUser = await getUser({
|
||||
elasticsearch,
|
||||
kibana,
|
||||
username: user.username,
|
||||
});
|
||||
if (!existingUser) {
|
||||
createUser({ elasticsearch, newUser: user, securityService });
|
||||
return;
|
||||
}
|
||||
|
||||
updateUser({
|
||||
existingUser,
|
||||
newUser: user,
|
||||
securityService,
|
||||
});
|
||||
}
|
||||
|
||||
async function createUser({
|
||||
elasticsearch,
|
||||
newUser,
|
||||
securityService,
|
||||
}: {
|
||||
elasticsearch: Elasticsearch;
|
||||
newUser: User;
|
||||
securityService: SecurityService;
|
||||
}) {
|
||||
const { username, ...options } = newUser;
|
||||
await securityService.user.create(username, {
|
||||
...options,
|
||||
enabled: true,
|
||||
password: elasticsearch.password,
|
||||
});
|
||||
}
|
||||
|
||||
async function updateUser({
|
||||
existingUser,
|
||||
newUser,
|
||||
securityService,
|
||||
}: {
|
||||
existingUser: User;
|
||||
newUser: User;
|
||||
securityService: SecurityService;
|
||||
}) {
|
||||
const { username } = newUser;
|
||||
const allRoles = union(existingUser.roles, newUser.roles);
|
||||
const hasAllRoles = difference(allRoles, existingUser.roles).length === 0;
|
||||
if (hasAllRoles) {
|
||||
console.log(`Skipping: User "${username}" already has necessary roles: "${newUser.roles}"`);
|
||||
return;
|
||||
}
|
||||
|
||||
const { username: _, ...options } = existingUser;
|
||||
await securityService.user.create(username, { ...options, roles: allRoles });
|
||||
}
|
||||
|
||||
async function getUser({
|
||||
elasticsearch,
|
||||
kibana,
|
||||
username,
|
||||
}: {
|
||||
elasticsearch: Elasticsearch;
|
||||
kibana: Kibana;
|
||||
username: string;
|
||||
}) {
|
||||
try {
|
||||
return await callKibana<User>({
|
||||
elasticsearch,
|
||||
kibana,
|
||||
options: {
|
||||
url: `/internal/security/users/${username}`,
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
// return empty if user doesn't exist
|
||||
if (isAxiosError(e) && e.response?.status === 404) {
|
||||
return null;
|
||||
}
|
||||
|
||||
throw e;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,108 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import { asyncForEach } from '@kbn/std';
|
||||
import { ProfilingUsername, profilingUsers } from './authentication';
|
||||
import { AbortError, callKibana } from './helpers/call_kibana';
|
||||
import { createOrUpdateUser } from './helpers/create_or_update_user';
|
||||
import { SecurityService } from '../../../../../test/common/services/security/security';
|
||||
|
||||
export interface Elasticsearch {
|
||||
node: string;
|
||||
username: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface Kibana {
|
||||
hostname: string;
|
||||
}
|
||||
|
||||
export async function createProfilingUsers({
|
||||
kibana,
|
||||
elasticsearch,
|
||||
securityService,
|
||||
}: {
|
||||
kibana: Kibana;
|
||||
elasticsearch: Elasticsearch;
|
||||
securityService: SecurityService;
|
||||
}) {
|
||||
const isCredentialsValid = await getIsCredentialsValid({
|
||||
elasticsearch,
|
||||
kibana,
|
||||
});
|
||||
|
||||
if (!isCredentialsValid) {
|
||||
throw new AbortError('Invalid username/password');
|
||||
}
|
||||
|
||||
const isSecurityEnabled = await getIsSecurityEnabled({
|
||||
elasticsearch,
|
||||
kibana,
|
||||
});
|
||||
|
||||
if (!isSecurityEnabled) {
|
||||
throw new AbortError('Security must be enabled!');
|
||||
}
|
||||
|
||||
const userNames = Object.values(ProfilingUsername);
|
||||
await asyncForEach(userNames, async (username) => {
|
||||
const user = profilingUsers[username];
|
||||
const { builtInRoleNames = [] } = user;
|
||||
|
||||
// create user
|
||||
await createOrUpdateUser({
|
||||
securityService,
|
||||
elasticsearch,
|
||||
kibana,
|
||||
user: { username, roles: builtInRoleNames },
|
||||
});
|
||||
});
|
||||
|
||||
return userNames;
|
||||
}
|
||||
|
||||
async function getIsSecurityEnabled({
|
||||
elasticsearch,
|
||||
kibana,
|
||||
}: {
|
||||
elasticsearch: Elasticsearch;
|
||||
kibana: Kibana;
|
||||
}) {
|
||||
try {
|
||||
await callKibana({
|
||||
elasticsearch,
|
||||
kibana,
|
||||
options: {
|
||||
url: `/internal/security/me`,
|
||||
},
|
||||
});
|
||||
return true;
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function getIsCredentialsValid({
|
||||
elasticsearch,
|
||||
kibana,
|
||||
}: {
|
||||
elasticsearch: Elasticsearch;
|
||||
kibana: Kibana;
|
||||
}) {
|
||||
try {
|
||||
await callKibana({
|
||||
elasticsearch,
|
||||
kibana,
|
||||
options: {
|
||||
validateStatus: (status) => status >= 200 && status < 400,
|
||||
url: `/`,
|
||||
},
|
||||
});
|
||||
return true;
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
}
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { GenericFtrProviderContext } from '@kbn/test';
|
||||
import { ProfilingServices } from './config';
|
||||
import { FtrProviderContext as InheritedFtrProviderContext } from '../../api_integration/ftr_provider_context';
|
||||
|
||||
export type InheritedServices = InheritedFtrProviderContext extends GenericFtrProviderContext<
|
||||
infer TServices,
|
||||
{}
|
||||
>
|
||||
? TServices
|
||||
: {};
|
||||
|
||||
export type { InheritedFtrProviderContext };
|
||||
export type FtrProviderContext = GenericFtrProviderContext<ProfilingServices, {}>;
|
174
x-pack/test/profiling_api_integration/common/registry.ts
Normal file
174
x-pack/test/profiling_api_integration/common/registry.ts
Normal file
|
@ -0,0 +1,174 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { joinByKey } from '@kbn/apm-plugin/common/utils/join_by_key';
|
||||
import { maybe } from '@kbn/apm-plugin/common/utils/maybe';
|
||||
import callsites from 'callsites';
|
||||
import { castArray, groupBy } from 'lodash';
|
||||
import Path from 'path';
|
||||
import fs from 'fs';
|
||||
import { ProfilingFtrConfigName } from '../configs';
|
||||
import { FtrProviderContext } from './ftr_provider_context';
|
||||
|
||||
const esArchiversPath = Path.posix.join(__dirname, 'fixtures', 'es_archiver', 'profiling');
|
||||
|
||||
interface RunCondition {
|
||||
config: ProfilingFtrConfigName;
|
||||
}
|
||||
|
||||
export function RegistryProvider({ getService }: FtrProviderContext) {
|
||||
const profilingFtrConfig = getService('profilingFtrConfig');
|
||||
const es = getService('es');
|
||||
|
||||
const callbacks: Array<
|
||||
RunCondition & {
|
||||
runs: Array<{
|
||||
cb: () => void;
|
||||
}>;
|
||||
}
|
||||
> = [];
|
||||
|
||||
let running: boolean = false;
|
||||
|
||||
function when(
|
||||
title: string,
|
||||
conditions: RunCondition | RunCondition[],
|
||||
callback: (condition: RunCondition) => void,
|
||||
skip?: boolean
|
||||
) {
|
||||
const allConditions = castArray(conditions);
|
||||
|
||||
if (!allConditions.length) {
|
||||
throw new Error('At least one condition should be defined');
|
||||
}
|
||||
|
||||
if (running) {
|
||||
throw new Error("Can't add tests when running");
|
||||
}
|
||||
|
||||
const frame = maybe(callsites()[1]);
|
||||
|
||||
const file = frame?.getFileName();
|
||||
|
||||
if (!file) {
|
||||
throw new Error('Could not infer file for suite');
|
||||
}
|
||||
|
||||
allConditions.forEach((matchedCondition) => {
|
||||
callbacks.push({
|
||||
...matchedCondition,
|
||||
runs: [
|
||||
{
|
||||
cb: () => {
|
||||
const suite: ReturnType<typeof describe> = (skip ? describe.skip : describe)(
|
||||
title,
|
||||
() => {
|
||||
callback(matchedCondition);
|
||||
}
|
||||
) as any;
|
||||
|
||||
suite.file = file;
|
||||
suite.eachTest((test) => {
|
||||
test.file = file;
|
||||
});
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
when.skip = (
|
||||
title: string,
|
||||
conditions: RunCondition | RunCondition[],
|
||||
callback: (condition: RunCondition) => void
|
||||
) => {
|
||||
when(title, conditions, callback, true);
|
||||
};
|
||||
|
||||
const registry = {
|
||||
when,
|
||||
run: () => {
|
||||
running = true;
|
||||
|
||||
const logger = getService('log');
|
||||
|
||||
const logWithTimer = () => {
|
||||
const start = process.hrtime();
|
||||
|
||||
return (message: string) => {
|
||||
const diff = process.hrtime(start);
|
||||
const time = `${Math.round(diff[0] * 1000 + diff[1] / 1e6)}ms`;
|
||||
logger.info(`(${time}) ${message}`);
|
||||
};
|
||||
};
|
||||
|
||||
const groups = joinByKey(callbacks, ['config'], (a, b) => ({
|
||||
...a,
|
||||
...b,
|
||||
runs: a.runs.concat(b.runs),
|
||||
}));
|
||||
|
||||
callbacks.length = 0;
|
||||
|
||||
const byConfig = groupBy(groups, 'config');
|
||||
|
||||
Object.keys(byConfig).forEach((config) => {
|
||||
const groupsForConfig = byConfig[config];
|
||||
|
||||
// register suites for other configs, but skip them so tests are marked as such
|
||||
// and their snapshots are not marked as obsolete
|
||||
(config === profilingFtrConfig.name ? describe : describe.skip)(config, () => {
|
||||
groupsForConfig.forEach((group) => {
|
||||
const { runs } = group;
|
||||
|
||||
const runBefore = async () => {
|
||||
const log = logWithTimer();
|
||||
const content = fs.readFileSync(`${esArchiversPath}/data.json`, 'utf8');
|
||||
log(`Loading profiling data`);
|
||||
await es.bulk({ operations: content.split('\n'), refresh: 'wait_for' });
|
||||
log('Loaded profiling data');
|
||||
};
|
||||
|
||||
const runAfter = async () => {
|
||||
const log = logWithTimer();
|
||||
log(`Unloading Profiling data`);
|
||||
const indices = await es.cat.indices({ format: 'json' });
|
||||
const profilingIndices = indices
|
||||
.filter((index) => index.index !== undefined)
|
||||
.map((index) => index.index)
|
||||
.filter((index) => {
|
||||
return index!.startsWith('profiling') || index!.startsWith('.profiling');
|
||||
}) as string[];
|
||||
await Promise.all([
|
||||
...profilingIndices.map((index) => es.indices.delete({ index })),
|
||||
es.indices.deleteDataStream({
|
||||
name: 'profiling-events*',
|
||||
}),
|
||||
]);
|
||||
log('Unloaded Profiling data');
|
||||
};
|
||||
|
||||
describe('Loading profiling data', () => {
|
||||
before(runBefore);
|
||||
|
||||
runs.forEach((run) => {
|
||||
run.cb();
|
||||
});
|
||||
|
||||
after(runAfter);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
running = false;
|
||||
},
|
||||
};
|
||||
|
||||
return registry;
|
||||
}
|
30
x-pack/test/profiling_api_integration/configs/ftr_kibana.yml
Normal file
30
x-pack/test/profiling_api_integration/configs/ftr_kibana.yml
Normal file
|
@ -0,0 +1,30 @@
|
|||
xpack.profiling.enabled: true
|
||||
|
||||
xpack.cloud.id: 'foo'
|
||||
xpack.fleet.packages:
|
||||
- name: apm
|
||||
version: latest
|
||||
|
||||
xpack.fleet.agentPolicies:
|
||||
- name: Elastic APM
|
||||
id: policy-elastic-agent-on-cloud
|
||||
package_policies:
|
||||
- name: Elastic APM
|
||||
id: elastic-cloud-apm
|
||||
package:
|
||||
name: apm
|
||||
inputs:
|
||||
- type: apm
|
||||
keep_enabled: true
|
||||
vars:
|
||||
- name: api_key_enabled
|
||||
value: true
|
||||
- name: host
|
||||
value: '0.0.0.0:8200'
|
||||
frozen: true
|
||||
- name: secret_token
|
||||
value: 'foo'
|
||||
- name: profiling.enabled
|
||||
value: true
|
||||
- name: profiling.metrics.elasticsearch
|
||||
value: ['https://elasticsearch:9200']
|
40
x-pack/test/profiling_api_integration/configs/index.ts
Normal file
40
x-pack/test/profiling_api_integration/configs/index.ts
Normal file
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { mapValues } from 'lodash';
|
||||
import path from 'path';
|
||||
import { createTestConfig, CreateTestConfig } from '../common/config';
|
||||
|
||||
const kibanaYamlFilePath = path.join(__dirname, './ftr_kibana.yml');
|
||||
|
||||
const profilingDebugLogger = {
|
||||
name: 'plugins.profiling',
|
||||
level: 'debug',
|
||||
appenders: ['console'],
|
||||
};
|
||||
|
||||
const profilingFtrConfigs = {
|
||||
cloud: {
|
||||
license: 'trial' as const,
|
||||
kibanaConfig: {
|
||||
'logging.loggers': [profilingDebugLogger],
|
||||
config: kibanaYamlFilePath,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export type ProfilingFtrConfigName = keyof typeof profilingFtrConfigs;
|
||||
|
||||
export const configs: Record<ProfilingFtrConfigName, CreateTestConfig> = mapValues(
|
||||
profilingFtrConfigs,
|
||||
(value, key) => {
|
||||
return createTestConfig({
|
||||
name: key as ProfilingFtrConfigName,
|
||||
...value,
|
||||
});
|
||||
}
|
||||
);
|
|
@ -0,0 +1,127 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import expect from '@kbn/expect';
|
||||
import { getRoutePaths } from '@kbn/profiling-plugin/common';
|
||||
import { ProfilingApiError } from '../common/api_supertest';
|
||||
import { getProfilingApiClient } from '../common/config';
|
||||
import { FtrProviderContext } from '../common/ftr_provider_context';
|
||||
|
||||
const profilingRoutePaths = getRoutePaths();
|
||||
|
||||
export default function featureControlsTests({ getService }: FtrProviderContext) {
|
||||
const registry = getService('registry');
|
||||
const profilingApiClient = getService('profilingApiClient');
|
||||
const log = getService('log');
|
||||
|
||||
const start = encodeURIComponent(new Date(Date.now() - 10000).valueOf());
|
||||
const end = encodeURIComponent(new Date().valueOf());
|
||||
|
||||
const expect403 = (status: number) => {
|
||||
expect(status).to.be(403);
|
||||
};
|
||||
|
||||
const expect200 = (status: number) => {
|
||||
expect(status).to.be(200);
|
||||
};
|
||||
|
||||
interface Endpoint {
|
||||
url: string;
|
||||
method?: 'GET' | 'POST';
|
||||
params?: { query: Record<string, any> };
|
||||
body?: any;
|
||||
}
|
||||
|
||||
const endpoints: Endpoint[] = [
|
||||
{
|
||||
url: profilingRoutePaths.TopNContainers,
|
||||
params: { query: { timeFrom: start, timeTo: end, kuery: '' } },
|
||||
},
|
||||
{
|
||||
url: profilingRoutePaths.TopNDeployments,
|
||||
params: { query: { timeFrom: start, timeTo: end, kuery: '' } },
|
||||
},
|
||||
{
|
||||
url: profilingRoutePaths.TopNHosts,
|
||||
params: { query: { timeFrom: start, timeTo: end, kuery: '' } },
|
||||
},
|
||||
{
|
||||
url: profilingRoutePaths.TopNTraces,
|
||||
params: { query: { timeFrom: start, timeTo: end, kuery: '' } },
|
||||
},
|
||||
{
|
||||
url: profilingRoutePaths.TopNThreads,
|
||||
params: { query: { timeFrom: start, timeTo: end, kuery: '' } },
|
||||
},
|
||||
{
|
||||
url: profilingRoutePaths.TopNFunctions,
|
||||
params: { query: { timeFrom: start, timeTo: end, kuery: '', startIndex: 1, endIndex: 5 } },
|
||||
},
|
||||
{
|
||||
url: profilingRoutePaths.Flamechart,
|
||||
params: { query: { timeFrom: start, timeTo: end, kuery: '' } },
|
||||
},
|
||||
{ url: profilingRoutePaths.SetupDataCollectionInstructions },
|
||||
{ url: profilingRoutePaths.HasSetupESResources },
|
||||
];
|
||||
|
||||
async function executeRequests({
|
||||
expectation,
|
||||
runAsUser,
|
||||
}: {
|
||||
expectation: 'forbidden' | 'response';
|
||||
runAsUser: Awaited<ReturnType<typeof getProfilingApiClient>>;
|
||||
}) {
|
||||
for (const endpoint of endpoints) {
|
||||
const method = endpoint.method || 'GET';
|
||||
const endpointPath = `${method} ${endpoint.url}`;
|
||||
try {
|
||||
log.info(`Requesting: ${endpointPath}. Expecting: ${expectation}`);
|
||||
const result = await runAsUser({
|
||||
endpoint: endpointPath,
|
||||
params: endpoint.params || {},
|
||||
});
|
||||
|
||||
if (expectation === 'forbidden') {
|
||||
throw new Error(
|
||||
`Endpoint: ${endpointPath}
|
||||
Status code: ${result.status}
|
||||
Response: ${result.body}`
|
||||
);
|
||||
}
|
||||
|
||||
expect200(result.status);
|
||||
} catch (e) {
|
||||
if (e instanceof ProfilingApiError && expectation === 'forbidden') {
|
||||
expect403(e.res.status);
|
||||
} else {
|
||||
throw new Error(
|
||||
`Endpoint: ${endpointPath}
|
||||
Status code: ${e.res.status}
|
||||
Response: ${e.res}
|
||||
|
||||
${e.message}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
registry.when('Profiling feature controls', { config: 'cloud' }, () => {
|
||||
before(async () => {});
|
||||
it(`returns forbidden for users with no access to profiling APIs`, async () => {
|
||||
await executeRequests({
|
||||
runAsUser: profilingApiClient.noAccessUser,
|
||||
expectation: 'forbidden',
|
||||
});
|
||||
});
|
||||
|
||||
it(`returns ok for users with access to profiling APIs`, async () => {
|
||||
await executeRequests({ runAsUser: profilingApiClient.readUser, expectation: 'response' });
|
||||
});
|
||||
});
|
||||
}
|
50
x-pack/test/profiling_api_integration/tests/index.ts
Normal file
50
x-pack/test/profiling_api_integration/tests/index.ts
Normal file
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import globby from 'globby';
|
||||
import path from 'path';
|
||||
import { FtrProviderContext } from '../common/ftr_provider_context';
|
||||
|
||||
const cwd = path.join(__dirname);
|
||||
const envGrepFiles = process.env.profiling_TEST_GREP_FILES as string;
|
||||
|
||||
function getGlobPattern() {
|
||||
try {
|
||||
const envGrepFilesParsed = JSON.parse(envGrepFiles as string) as string[];
|
||||
return envGrepFilesParsed.map((pattern) => `**/${pattern}**`);
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
return '**/*.spec.ts';
|
||||
}
|
||||
|
||||
export default function profilingApiIntegrationTests({
|
||||
getService,
|
||||
loadTestFile,
|
||||
}: FtrProviderContext) {
|
||||
const registry = getService('registry');
|
||||
|
||||
describe('Profiling API tests', function () {
|
||||
const filePattern = getGlobPattern();
|
||||
const tests = globby.sync(filePattern, { cwd });
|
||||
|
||||
if (envGrepFiles) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(
|
||||
`\nCommand "--grep-files=${filePattern}" matched ${tests.length} file(s):\n${tests
|
||||
.map((name) => ` - ${name}`)
|
||||
.join('\n')}\n`
|
||||
);
|
||||
}
|
||||
|
||||
tests.forEach((testName) => {
|
||||
describe(testName, () => {
|
||||
loadTestFile(require.resolve(`./${testName}`));
|
||||
registry.run();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -129,6 +129,7 @@
|
|||
"@kbn/core-http-common",
|
||||
"@kbn/slo-schema",
|
||||
"@kbn/lens-plugin",
|
||||
"@kbn/telemetry-tools"
|
||||
"@kbn/telemetry-tools",
|
||||
"@kbn/profiling-plugin"
|
||||
]
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue