[APM] Refactor Cypress e2e tests (#109024)

* Improve script to setup users and roles

* fix readme

* CI fixes

* add index permissions to roles

* disable welcome screen

* Run es archive once before tests

* Fix ts issues

* Update x-pack/plugins/apm/readme.md

Co-authored-by: Nathan L Smith <nathan.smith@elastic.co>

Co-authored-by: Nathan L Smith <nathan.smith@elastic.co>
This commit is contained in:
Søren Louv-Jansen 2021-08-19 16:46:25 +02:00 committed by GitHub
parent d522cae193
commit a93a7efa80
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
33 changed files with 453 additions and 413 deletions

View file

@ -33,6 +33,7 @@ async function config({ readConfigFile }: FtrConfigProviderContext) {
...xpackFunctionalTestsConfig.get('kbnTestServer'),
serverArgs: [
...xpackFunctionalTestsConfig.get('kbnTestServer.serverArgs'),
'--home.disableWelcomeScreen=true',
'--csp.strict=false',
// define custom kibana server args here
`--elasticsearch.ssl.certificateAuthorities=${CA_CERT_PATH}`,

View file

@ -7,7 +7,6 @@
import url from 'url';
import archives_metadata from '../../fixtures/es_archiver/archives_metadata';
import { esArchiverLoad, esArchiverUnload } from '../../tasks/es_archiver';
const { start, end } = archives_metadata['apm_8.0.0'];
@ -28,15 +27,10 @@ const apisToIntercept = [
];
describe('Home page', () => {
before(() => {
esArchiverLoad('apm_8.0.0');
});
after(() => {
esArchiverUnload('apm_8.0.0');
});
beforeEach(() => {
cy.loginAsReadOnlyUser();
});
it('Redirects to service page with rangeFrom and rangeTo added to the URL', () => {
cy.visit('/app/apm');

View file

@ -6,7 +6,6 @@
*/
import url from 'url';
import archives_metadata from '../../../fixtures/es_archiver/archives_metadata';
import { esArchiverLoad, esArchiverUnload } from '../../../tasks/es_archiver';
const { start, end } = archives_metadata['apm_8.0.0'];
@ -59,15 +58,10 @@ const apisToIntercept = [
];
describe('Service overview - header filters', () => {
before(() => {
esArchiverLoad('apm_8.0.0');
});
after(() => {
esArchiverUnload('apm_8.0.0');
});
beforeEach(() => {
cy.loginAsReadOnlyUser();
});
describe('Filtering by transaction type', () => {
it('changes url when selecting different value', () => {
cy.visit(serviceOverviewHref);

View file

@ -7,7 +7,6 @@
import url from 'url';
import archives_metadata from '../../../fixtures/es_archiver/archives_metadata';
import { esArchiverLoad, esArchiverUnload } from '../../../tasks/es_archiver';
const { start, end } = archives_metadata['apm_8.0.0'];
@ -43,25 +42,21 @@ describe('Instances table', () => {
beforeEach(() => {
cy.loginAsReadOnlyUser();
});
describe('when data is not loaded', () => {
it('shows empty message', () => {
cy.visit(serviceOverviewHref);
cy.contains('opbeans-java');
cy.get('[data-test-subj="serviceInstancesTableContainer"]').contains(
'No items found'
);
});
});
// describe('when data is not loaded', () => {
// it('shows empty message', () => {
// cy.visit(serviceOverviewHref);
// cy.contains('opbeans-java');
// cy.get('[data-test-subj="serviceInstancesTableContainer"]').contains(
// 'No items found'
// );
// });
// });
describe('when data is loaded', () => {
before(() => {
esArchiverLoad('apm_8.0.0');
});
after(() => {
esArchiverUnload('apm_8.0.0');
});
const serviceNodeName =
'31651f3c624b81c55dd4633df0b5b9f9ab06b151121b0404ae796632cd1f87ad';
it('has data in the table', () => {
cy.visit(serviceOverviewHref);
cy.contains('opbeans-java');

View file

@ -7,7 +7,6 @@
import url from 'url';
import archives_metadata from '../../../fixtures/es_archiver/archives_metadata';
import { esArchiverLoad, esArchiverUnload } from '../../../tasks/es_archiver';
const { start, end } = archives_metadata['apm_8.0.0'];
@ -18,15 +17,10 @@ const baseUrl = url.format({
});
describe('Service Overview', () => {
before(() => {
esArchiverLoad('apm_8.0.0');
});
after(() => {
esArchiverUnload('apm_8.0.0');
});
beforeEach(() => {
cy.loginAsReadOnlyUser();
});
it('persists transaction type selected when clicking on Transactions tab', () => {
cy.visit(baseUrl);
cy.get('[data-test-subj="headerFilterTransactionType"]').should(

View file

@ -7,7 +7,6 @@
import url from 'url';
import moment from 'moment';
import archives_metadata from '../../../fixtures/es_archiver/archives_metadata';
import { esArchiverLoad, esArchiverUnload } from '../../../tasks/es_archiver';
const { start, end } = archives_metadata['apm_8.0.0'];
@ -47,12 +46,6 @@ const apisToIntercept = [
];
describe('Service overview: Time Comparison', () => {
before(() => {
esArchiverLoad('apm_8.0.0');
});
after(() => {
esArchiverUnload('apm_8.0.0');
});
beforeEach(() => {
cy.loginAsReadOnlyUser();
});

View file

@ -7,7 +7,6 @@
import url from 'url';
import archives_metadata from '../../../fixtures/es_archiver/archives_metadata';
import { esArchiverLoad, esArchiverUnload } from '../../../tasks/es_archiver';
const { start, end } = archives_metadata['apm_8.0.0'];
@ -17,15 +16,10 @@ const serviceOverviewHref = url.format({
});
describe('Transactions Overview', () => {
before(() => {
esArchiverLoad('apm_8.0.0');
});
after(() => {
esArchiverUnload('apm_8.0.0');
});
beforeEach(() => {
cy.loginAsReadOnlyUser();
});
it('persists transaction type selected when navigating to Overview tab', () => {
cy.visit(serviceOverviewHref);
cy.get('[data-test-subj="headerFilterTransactionType"]').should(

View file

@ -5,4 +5,12 @@
* 2.0.
*/
Cypress.on('uncaught:exception', (err, runnable) => {
// @see https://stackoverflow.com/a/50387233/434980
// ResizeObserver error can be safely ignored
if (err.message.includes('ResizeObserver loop limit exceeded')) {
return false;
}
});
import './commands';

View file

@ -6,31 +6,32 @@
*/
import Path from 'path';
import { execSync } from 'child_process';
const ES_ARCHIVE_DIR = './cypress/fixtures/es_archiver';
// Otherwise cy.exec would inject NODE_TLS_REJECT_UNAUTHORIZED=0 and node would abort if used over https
// Otherwise execSync would inject NODE_TLS_REJECT_UNAUTHORIZED=0 and node would abort if used over https
const NODE_TLS_REJECT_UNAUTHORIZED = '1';
export const esArchiverLoad = (folder: string) => {
const path = Path.join(ES_ARCHIVE_DIR, folder);
cy.exec(
execSync(
`node ../../../../scripts/es_archiver load "${path}" --config ../../../test/functional/config.js`,
{ env: { NODE_TLS_REJECT_UNAUTHORIZED } }
{ env: { ...process.env, NODE_TLS_REJECT_UNAUTHORIZED } }
);
};
export const esArchiverUnload = (folder: string) => {
const path = Path.join(ES_ARCHIVE_DIR, folder);
cy.exec(
execSync(
`node ../../../../scripts/es_archiver unload "${path}" --config ../../../test/functional/config.js`,
{ env: { NODE_TLS_REJECT_UNAUTHORIZED } }
{ env: { ...process.env, NODE_TLS_REJECT_UNAUTHORIZED } }
);
};
export const esArchiverResetKibana = () => {
cy.exec(
execSync(
`node ../../../../scripts/es_archiver empty-kibana-index --config ../../../test/functional/config.js`,
{ env: { NODE_TLS_REJECT_UNAUTHORIZED }, failOnNonZeroExit: false }
{ env: { ...process.env, NODE_TLS_REJECT_UNAUTHORIZED } }
);
};

View file

@ -9,9 +9,9 @@ import { FtrConfigProviderContext } from '@kbn/test';
import { cypressOpenTests } from './cypress_start';
async function openE2ETests({ readConfigFile }: FtrConfigProviderContext) {
const cypressConfig = await readConfigFile(require.resolve('./config.ts'));
const kibanaConfig = await readConfigFile(require.resolve('./config.ts'));
return {
...cypressConfig.getAll(),
...kibanaConfig.getAll(),
testRunner: cypressOpenTests,
};
}

View file

@ -8,13 +8,13 @@ import { argv } from 'yargs';
import { FtrConfigProviderContext } from '@kbn/test';
import { cypressRunTests } from './cypress_start';
const spec = argv.grep as string;
const specArg = argv.spec as string | undefined;
async function runE2ETests({ readConfigFile }: FtrConfigProviderContext) {
const cypressConfig = await readConfigFile(require.resolve('./config.ts'));
const kibanaConfig = await readConfigFile(require.resolve('./config.ts'));
return {
...cypressConfig.getAll(),
testRunner: cypressRunTests(spec),
...kibanaConfig.getAll(),
testRunner: cypressRunTests(specArg),
};
}

View file

@ -9,7 +9,8 @@ import Url from 'url';
import cypress from 'cypress';
import { FtrProviderContext } from './ftr_provider_context';
import archives_metadata from './cypress/fixtures/es_archiver/archives_metadata';
import { createKibanaUserRole } from '../scripts/kibana-security/create_kibana_user_role';
import { createApmUsersAndRoles } from '../scripts/create-apm-users-and-roles/create_apm_users_and_roles';
import { esArchiverLoad, esArchiverUnload } from './cypress/tasks/es_archiver';
export function cypressRunTests(spec?: string) {
return async ({ getService }: FtrProviderContext) => {
@ -47,7 +48,7 @@ async function cypressStart(
});
// Creates APM users
await createKibanaUserRole({
await createApmUsersAndRoles({
elasticsearch: {
username: config.get('servers.elasticsearch.username'),
password: config.get('servers.elasticsearch.password'),
@ -58,8 +59,10 @@ async function cypressStart(
},
});
return cypressExecution({
...(spec !== 'undefined' ? { spec } : {}),
await esArchiverLoad('apm_8.0.0');
const res = await cypressExecution({
...(spec !== undefined ? { spec } : {}),
config: { baseUrl: kibanaUrl },
env: {
START_DATE: start,
@ -67,4 +70,8 @@ async function cypressStart(
KIBANA_URL: kibanaUrl,
},
});
await esArchiverUnload('apm_8.0.0');
return res;
}

View file

@ -255,7 +255,7 @@ exports[`ErrorGroupOverview -> List should render empty state 1`] = `
<span
className="euiTableCellContent__text"
>
No errors were found
No errors found
</span>
</div>
</td>

View file

@ -195,7 +195,7 @@ function ErrorGroupList({ items, serviceName }: Props) {
return (
<ManagedTable
noItemsMessage={i18n.translate('xpack.apm.errorsTable.noErrorsLabel', {
defaultMessage: 'No errors were found',
defaultMessage: 'No errors found',
})}
items={items}
columns={columns}

View file

@ -209,6 +209,17 @@ export function ServiceOverviewErrorsTable({ serviceName }: Props) {
}
>
<EuiBasicTable
noItemsMessage={
status === FETCH_STATUS.LOADING
? i18n.translate(
'xpack.apm.serviceOverview.errorsTable.loading',
{ defaultMessage: 'Loading...' }
)
: i18n.translate(
'xpack.apm.serviceOverview.errorsTable.noResults',
{ defaultMessage: 'No errors found' }
)
}
columns={columns}
items={items}
pagination={{

View file

@ -146,6 +146,15 @@ export function ServiceOverviewInstancesTable({
isEmptyAndLoading={mainStatsItemCount === 0 && isLoading}
>
<EuiBasicTable
noItemsMessage={
isLoading
? i18n.translate('xpack.apm.serviceOverview.loadingText', {
defaultMessage: 'No instances found',
})
: i18n.translate('xpack.apm.serviceOverview.noResultsText', {
defaultMessage: 'No instances found',
})
}
data-test-subj="instancesTable"
loading={isLoading}
items={mainStatsItems}

View file

@ -5,6 +5,7 @@
* 2.0.
*/
import { i18n } from '@kbn/i18n';
import { EuiBasicTable, EuiBasicTableColumn } from '@elastic/eui';
import { orderBy } from 'lodash';
import React, { ReactNode, useCallback, useMemo } from 'react';
@ -129,7 +130,13 @@ function UnoptimizedManagedTable<T>(props: Props<T>) {
return (
<EuiBasicTable
loading={isLoading}
noItemsMessage={noItemsMessage}
noItemsMessage={
isLoading
? i18n.translate('xpack.apm.managedTable.loading', {
defaultMessage: 'Loading...',
})
: noItemsMessage
}
items={renderedItems}
columns={(columns as unknown) as Array<EuiBasicTableColumn<T>>} // EuiBasicTableColumn is stricter than ITableColumn
sorting={sort}

View file

@ -234,12 +234,9 @@ export function TransactionsTable({
<EuiFlexItem grow={false}>
<EuiTitle size="xs">
<h2>
{i18n.translate(
'xpack.apm.serviceOverview.transactionsTableTitle',
{
defaultMessage: 'Transactions',
}
)}
{i18n.translate('xpack.apm.transactionsTable.title', {
defaultMessage: 'Transactions',
})}
</h2>
</EuiTitle>
</EuiFlexItem>
@ -250,12 +247,9 @@ export function TransactionsTable({
latencyAggregationType={latencyAggregationType}
transactionType={transactionType}
>
{i18n.translate(
'xpack.apm.serviceOverview.transactionsTableLinkText',
{
defaultMessage: 'View transactions',
}
)}
{i18n.translate('xpack.apm.transactionsTable.linkText', {
defaultMessage: 'View transactions',
})}
</TransactionOverviewLink>
</EuiFlexItem>
)}
@ -265,7 +259,7 @@ export function TransactionsTable({
<EuiFlexItem>
<EuiCallOut
title={i18n.translate(
'xpack.apm.transactionCardinalityWarning.title',
'xpack.apm.transactionsTable.cardinalityWarning.title',
{
defaultMessage:
'This view shows a subset of reported transactions.',
@ -276,7 +270,7 @@ export function TransactionsTable({
>
<p>
<FormattedMessage
id="xpack.apm.transactionCardinalityWarning.body"
id="xpack.apm.transactionsTable.cardinalityWarning.body"
defaultMessage="The number of unique transaction names exceeds the configured value of {bucketSize}. Try reconfiguring your agents to group similar transactions or increase the value of {codeBlock}"
values={{
bucketSize,
@ -291,7 +285,7 @@ export function TransactionsTable({
path="/troubleshooting.html#troubleshooting-too-many-transactions"
>
{i18n.translate(
'xpack.apm.transactionCardinalityWarning.docsLink',
'xpack.apm.transactionsTable.cardinalityWarning.docsLink',
{ defaultMessage: 'Learn more in the docs' }
)}
</ElasticDocsLink>
@ -307,6 +301,15 @@ export function TransactionsTable({
isEmptyAndLoading={transactionGroupsTotalItems === 0 && isLoading}
>
<EuiBasicTable
noItemsMessage={
isLoading
? i18n.translate('xpack.apm.transactionsTable.loading', {
defaultMessage: 'Loading...',
})
: i18n.translate('xpack.apm.transactionsTable.noResults', {
defaultMessage: 'No transaction groups found',
})
}
loading={isLoading}
items={transactionGroups}
columns={columns}

View file

@ -138,22 +138,17 @@ node scripts/eslint.js x-pack/legacy/plugins/apm
## Setup default APM users
APM behaves differently depending on which the role and permissions a logged in user has.
For testing purposes APM uses 3 custom users:
**apm_read_user**: Apps: read. Indices: read (`apm-*`)
**apm_write_user**: Apps: read/write. Indices: read (`apm-*`)
**kibana_write_user** Apps: read/write. Indices: None
To create the users with the correct roles run the following script:
APM behaves differently depending on which the role and permissions a logged in user has. To create the users run:
```sh
node x-pack/plugins/apm/scripts/setup-kibana-security.js --role-suffix <github-username-or-something-unique>
node x-pack/plugins/apm/scripts/create-apm-users-and-roles.js --username elastic --password changeme --kibana-url http://localhost:5601 --role-suffix <github-username-or-something-unique>
```
The users will be created with the password specified in kibana.dev.yml for `elasticsearch.password`
This will create:
**apm_read_user**: Read only user
**apm_power_user**: Read+write user.
## Debugging Elasticsearch queries

View file

@ -13,11 +13,11 @@
* The two roles will be assigned to the already existing users: `apm_read_user`, `apm_write_user`, `kibana_write_user`
*
* This makes it possible to use the existing cloud users locally
* Usage: node setup-kibana-security.js --role-suffix <YOUR-GITHUB-USERNAME-OR-SOMETHING-UNIQUE>
* Usage: node create-apm-users-and-roles.js --role-suffix <YOUR-GITHUB-USERNAME-OR-SOMETHING-UNIQUE>
******************************/
// compile typescript on the fly
// eslint-disable-next-line import/no-extraneous-dependencies
require('@kbn/optimizer').registerNodeAutoTranspilation();
require('./kibana-security/setup-custom-kibana-user-role.ts');
require('./create-apm-users-and-roles/create_apm_users_and_roles_cli.ts');

View file

@ -0,0 +1,88 @@
/*
* 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 { AbortError, callKibana } from './helpers/call_kibana';
import { createRole } from './helpers/create_role';
import { powerUserRole } from './roles/power_user_role';
import { readOnlyUserRole } from './roles/read_only_user_role';
import { createOrUpdateUser } from './helpers/create_or_update_user';
export interface Elasticsearch {
username: string;
password: string;
}
export interface Kibana {
roleSuffix: string;
hostname: string;
}
export async function createApmUsersAndRoles({
kibana,
elasticsearch,
}: {
kibana: Kibana;
elasticsearch: Elasticsearch;
}) {
const isSecurityEnabled = await getIsSecurityEnabled({
elasticsearch,
kibana,
});
if (!isSecurityEnabled) {
throw new AbortError('Security must be enabled!');
}
const KIBANA_READ_ROLE = `kibana_read_${kibana.roleSuffix}`;
const KIBANA_POWER_ROLE = `kibana_power_${kibana.roleSuffix}`;
// roles definition
const roles = [
{ roleName: KIBANA_READ_ROLE, role: readOnlyUserRole },
{ roleName: KIBANA_POWER_ROLE, role: powerUserRole },
];
// create roles
await Promise.all(
roles.map(async (role) => createRole({ elasticsearch, kibana, ...role }))
);
// user definitions
const users = [
{ username: 'apm_read_user', roles: [KIBANA_READ_ROLE] },
{ username: 'apm_power_user', roles: [KIBANA_POWER_ROLE] },
];
// create users
await Promise.all(
users.map(async (user) =>
createOrUpdateUser({ elasticsearch, kibana, user })
)
);
return users;
}
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;
}
}

View file

@ -0,0 +1,92 @@
/*
* 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 { argv } from 'yargs';
import { AbortError, isAxiosError } from './helpers/call_kibana';
import { createApmUsersAndRoles } from './create_apm_users_and_roles';
import { getKibanaVersion } from './helpers/get_version';
async function init() {
const esUserName = (argv.username as string) || 'elastic';
const esPassword = argv.password as string | undefined;
const kibanaBaseUrl = argv.kibanaUrl as string | undefined;
const kibanaRoleSuffix = argv.roleSuffix as string | undefined;
if (!esPassword) {
console.error(
'Please specify credentials for elasticsearch: `--username elastic --password abcd` '
);
process.exit();
}
if (!kibanaBaseUrl) {
console.error(
'Please specify the url for Kibana: `--kibana-url http://localhost:5601` '
);
process.exit();
}
if (
!kibanaBaseUrl.startsWith('https://') &&
!kibanaBaseUrl.startsWith('http://')
) {
console.error(
'Kibana url must be prefixed with http(s):// `--kibana-url http://localhost:5601`'
);
process.exit();
}
if (!kibanaRoleSuffix) {
console.error(
'Please specify a unique suffix that will be added to your roles with `--role-suffix <suffix>` '
);
process.exit();
}
const kibana = { roleSuffix: kibanaRoleSuffix, hostname: kibanaBaseUrl };
const elasticsearch = { username: esUserName, password: esPassword };
console.log({ kibana, elasticsearch });
const version = await getKibanaVersion({ elasticsearch, kibana });
console.log(`Connected to Kibana ${version}`);
const users = await createApmUsersAndRoles({ elasticsearch, kibana });
const credentials = users
.map((u) => ` - ${u.username} / ${esPassword}`)
.join('\n');
console.log(
`\nYou can now login to ${kibana.hostname} with:\n${credentials}`
);
}
init().catch((e) => {
if (e instanceof AbortError) {
console.error(e.message);
} else if (isAxiosError(e)) {
console.error(
`${e.config.method?.toUpperCase() || 'GET'} ${e.config.url} (Code: ${
e.response?.status
})`
);
if (e.response) {
console.error(
JSON.stringify(
{ request: e.config, response: e.response.data },
null,
2
)
);
}
} else {
console.error(e);
}
});

View file

@ -6,46 +6,51 @@
*/
import axios, { AxiosRequestConfig, AxiosError } from 'axios';
import { once } from 'lodash';
import { Elasticsearch } from './create_kibana_user_role';
import { Elasticsearch, Kibana } from '../create_apm_users_and_roles';
export async function callKibana<T>({
elasticsearch,
kibanaHostname,
kibana,
options,
}: {
elasticsearch: Elasticsearch;
kibanaHostname: string;
kibana: Kibana;
options: AxiosRequestConfig;
}): Promise<T> {
const kibanaBasePath = await getKibanaBasePath({ kibanaHostname });
const baseUrl = await getBaseUrl(kibana.hostname);
const { username, password } = elasticsearch;
const { data } = await axios.request({
...options,
baseURL: kibanaHostname + kibanaBasePath,
baseURL: baseUrl,
auth: { username, password },
headers: { 'kbn-xsrf': 'true', ...options.headers },
});
return data;
}
const getKibanaBasePath = once(
async ({ kibanaHostname }: { kibanaHostname: string }) => {
try {
await axios.request({ url: kibanaHostname, maxRedirects: 0 });
} catch (e) {
if (isAxiosError(e)) {
const location = e.response?.headers?.location;
const isBasePath = RegExp(/^\/\w{3}$/).test(location);
return isBasePath ? location : '';
}
throw e;
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}`;
}
return '';
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);
}
}

View file

@ -8,61 +8,8 @@
/* eslint-disable no-console */
import { difference, union } from 'lodash';
import { callKibana, isAxiosError } from '../call_kibana';
import { Elasticsearch, Kibana } from '../create_kibana_user_role';
import { createRole } from './create_role';
import { powerUserRole } from './power_user_role';
import { readOnlyUserRole } from './read_only_user_role';
export async function createAPMUsers({
kibana: { roleSuffix, hostname },
elasticsearch,
}: {
kibana: Kibana;
elasticsearch: Elasticsearch;
}) {
const KIBANA_READ_ROLE = `kibana_read_${roleSuffix}`;
const KIBANA_POWER_ROLE = `kibana_power_${roleSuffix}`;
const APM_USER_ROLE = 'apm_user';
// roles definition
const roles = [
{
roleName: KIBANA_READ_ROLE,
role: readOnlyUserRole,
},
{
roleName: KIBANA_POWER_ROLE,
role: powerUserRole,
},
];
// create roles
await Promise.all(
roles.map(async (role) =>
createRole({ elasticsearch, kibanaHostname: hostname, ...role })
)
);
// users definition
const users = [
{
username: 'apm_read_user',
roles: [APM_USER_ROLE, KIBANA_READ_ROLE],
},
{
username: 'apm_power_user',
roles: [APM_USER_ROLE, KIBANA_POWER_ROLE],
},
];
// create users
await Promise.all(
users.map(async (user) =>
createOrUpdateUser({ elasticsearch, kibanaHostname: hostname, user })
)
);
}
import { Elasticsearch, Kibana } from '../create_apm_users_and_roles';
import { callKibana, isAxiosError } from './call_kibana';
interface User {
username: string;
@ -72,27 +19,27 @@ interface User {
enabled?: boolean;
}
async function createOrUpdateUser({
export async function createOrUpdateUser({
elasticsearch,
kibanaHostname,
kibana,
user,
}: {
elasticsearch: Elasticsearch;
kibanaHostname: string;
kibana: Kibana;
user: User;
}) {
const existingUser = await getUser({
elasticsearch,
kibanaHostname,
kibana,
username: user.username,
});
if (!existingUser) {
return createUser({ elasticsearch, kibanaHostname, newUser: user });
return createUser({ elasticsearch, kibana, newUser: user });
}
return updateUser({
elasticsearch,
kibanaHostname,
kibana,
existingUser,
newUser: user,
});
@ -100,16 +47,16 @@ async function createOrUpdateUser({
async function createUser({
elasticsearch,
kibanaHostname,
kibana,
newUser,
}: {
elasticsearch: Elasticsearch;
kibanaHostname: string;
kibana: Kibana;
newUser: User;
}) {
const user = await callKibana<User>({
elasticsearch,
kibanaHostname,
kibana,
options: {
method: 'POST',
url: `/internal/security/users/${newUser.username}`,
@ -127,12 +74,12 @@ async function createUser({
async function updateUser({
elasticsearch,
kibanaHostname,
kibana,
existingUser,
newUser,
}: {
elasticsearch: Elasticsearch;
kibanaHostname: string;
kibana: Kibana;
existingUser: User;
newUser: User;
}) {
@ -149,7 +96,7 @@ async function updateUser({
// assign role to user
await callKibana({
elasticsearch,
kibanaHostname,
kibana,
options: {
method: 'POST',
url: `/internal/security/users/${username}`,
@ -162,17 +109,17 @@ async function updateUser({
async function getUser({
elasticsearch,
kibanaHostname,
kibana,
username,
}: {
elasticsearch: Elasticsearch;
kibanaHostname: string;
kibana: Kibana;
username: string;
}) {
try {
return await callKibana<User>({
elasticsearch,
kibanaHostname,
kibana,
options: {
url: `/internal/security/users/${username}`,
},

View file

@ -7,8 +7,8 @@
/* eslint-disable no-console */
import { Role } from '../../../../security/common/model';
import { callKibana, isAxiosError } from '../call_kibana';
import { Elasticsearch } from '../create_kibana_user_role';
import { callKibana, isAxiosError } from './call_kibana';
import { Elasticsearch, Kibana } from '../create_apm_users_and_roles';
type Privilege = [] | ['read'] | ['all'];
export interface KibanaPrivileges {
@ -20,18 +20,18 @@ export type RoleType = Omit<Role, 'name' | 'metadata'>;
export async function createRole({
elasticsearch,
kibanaHostname,
kibana,
roleName,
role,
}: {
elasticsearch: Elasticsearch;
kibanaHostname: string;
kibana: Kibana;
roleName: string;
role: RoleType;
}) {
const roleFound = await getRole({
elasticsearch,
kibanaHostname,
kibana,
roleName,
});
if (roleFound) {
@ -41,7 +41,7 @@ export async function createRole({
await callKibana({
elasticsearch,
kibanaHostname,
kibana,
options: {
method: 'PUT',
url: `/api/security/role/${roleName}`,
@ -52,24 +52,22 @@ export async function createRole({
},
});
console.log(
`Created role "${roleName}" with privilege "${JSON.stringify(role.kibana)}"`
);
console.log(`Created role "${roleName}"`);
}
async function getRole({
elasticsearch,
kibanaHostname,
kibana,
roleName,
}: {
elasticsearch: Elasticsearch;
kibanaHostname: string;
kibana: Kibana;
roleName: string;
}): Promise<Role | null> {
try {
return await callKibana({
elasticsearch,
kibanaHostname,
kibana,
options: {
method: 'GET',
url: `/api/security/role/${roleName}`,

View 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 { Elasticsearch, Kibana } from '../create_apm_users_and_roles';
import { AbortError } from './call_kibana';
import { callKibana, isAxiosError } from './call_kibana';
export async function getKibanaVersion({
elasticsearch,
kibana,
}: {
elasticsearch: Elasticsearch;
kibana: Kibana;
}) {
try {
const res: { version: { number: number } } = await callKibana({
elasticsearch,
kibana,
options: {
method: 'GET',
url: `/api/status`,
},
});
return res.version.number;
} catch (e) {
if (isAxiosError(e)) {
switch (e.response?.status) {
case 401:
throw new AbortError(
`Could not access Kibana with the provided credentials. Username: "${e.config.auth?.username}". Password: "${e.config.auth?.password}"`
);
case 404:
throw new AbortError(
`Could not get version on ${e.config.url} (Code: 404)`
);
default:
throw new AbortError(
`Cannot access Kibana on ${e.config.baseURL}. Please specify Kibana with: "--kibana-url <url>"`
);
}
}
throw e;
}
}

View file

@ -5,10 +5,39 @@
* 2.0.
*/
import { RoleType } from './create_role';
import { RoleType } from '../helpers/create_role';
export const powerUserRole: RoleType = {
elasticsearch: { cluster: [], indices: [], run_as: [] },
elasticsearch: {
run_as: [],
cluster: [],
indices: [
// apm
{
names: ['apm-*', 'logs-apm*', 'metrics-apm*', 'traces-apm*'],
privileges: ['read', 'view_index_metadata'],
},
{
names: ['observability-annotations'],
privileges: ['read', 'write', 'view_index_metadata'],
},
// logs
{
names: ['logs-*', 'filebeat-*', 'kibana_sample_data_logs*'],
privileges: ['read', 'view_index_metadata'],
},
// metrics
{
names: ['metrics-*', 'metricbeat-*'],
privileges: ['read', 'view_index_metadata'],
},
// uptime
{
names: ['heartbeat-*', 'synthetics-*'],
privileges: ['read', 'view_index_metadata'],
},
],
},
kibana: [
{
base: [],

View file

@ -5,10 +5,41 @@
* 2.0.
*/
import { RoleType } from './create_role';
import { RoleType } from '../helpers/create_role';
export const readOnlyUserRole: RoleType = {
elasticsearch: { cluster: [], indices: [], run_as: [] },
elasticsearch: {
run_as: [],
cluster: [],
indices: [
// apm
{
names: [
'apm-*',
'logs-apm*',
'metrics-apm*',
'traces-apm*',
'observability-annotations',
],
privileges: ['read', 'view_index_metadata'],
},
// logs
{
names: ['logs-*', 'filebeat-*', 'kibana_sample_data_logs*'],
privileges: ['read', 'view_index_metadata'],
},
// metrics
{
names: ['metrics-*', 'metricbeat-*'],
privileges: ['read', 'view_index_metadata'],
},
// uptime
{
names: ['heartbeat-*', 'synthetics-*'],
privileges: ['read', 'view_index_metadata'],
},
],
},
kibana: [
{
base: [],

View file

@ -9,11 +9,11 @@ const { argv } = require('yargs');
const childProcess = require('child_process');
const path = require('path');
const { spec } = argv;
const { grep } = argv;
const e2eDir = path.join(__dirname, '../../ftr_e2e');
childProcess.execSync(
`node ../../../../scripts/functional_tests --config ./cypress_run.ts --grep ${spec}`,
`node ../../../../scripts/functional_tests --config ./cypress_run.ts --grep ${grep}`,
{ cwd: e2eDir, stdio: 'inherit' }
);

View file

@ -1,112 +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
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { callKibana, isAxiosError } from './call_kibana';
import { createAPMUsers } from './create_apm_users';
/* eslint-disable no-console */
export interface Elasticsearch {
username: string;
password: string;
}
export interface Kibana {
roleSuffix: string;
hostname: string;
}
export async function createKibanaUserRole({
kibana,
elasticsearch,
}: {
kibana: Kibana;
elasticsearch: Elasticsearch;
}) {
const version = await getKibanaVersion({
elasticsearch,
kibanaHostname: kibana.hostname,
});
console.log(`Connected to Kibana ${version}`);
const isSecurityEnabled = await getIsSecurityEnabled({
elasticsearch,
kibanaHostname: kibana.hostname,
});
if (!isSecurityEnabled) {
throw new AbortError('Security must be enabled!');
}
await createAPMUsers({ kibana, elasticsearch });
}
async function getIsSecurityEnabled({
elasticsearch,
kibanaHostname,
}: {
elasticsearch: Elasticsearch;
kibanaHostname: string;
}) {
try {
await callKibana({
elasticsearch,
kibanaHostname,
options: {
url: `/internal/security/me`,
},
});
return true;
} catch (err) {
return false;
}
}
async function getKibanaVersion({
elasticsearch,
kibanaHostname,
}: {
elasticsearch: Elasticsearch;
kibanaHostname: string;
}) {
try {
const res: { version: { number: number } } = await callKibana({
elasticsearch,
kibanaHostname,
options: {
method: 'GET',
url: `/api/status`,
},
});
return res.version.number;
} catch (e) {
if (isAxiosError(e)) {
switch (e.response?.status) {
case 401:
throw new AbortError(
`Could not access Kibana with the provided credentials. Username: "${e.config.auth?.username}". Password: "${e.config.auth?.password}"`
);
case 404:
throw new AbortError(
`Could not get version on ${e.config.url} (Code: 404)`
);
default:
throw new AbortError(
`Cannot access Kibana on ${e.config.baseURL}. Please specify Kibana with: "--kibana-url <url>"`
);
}
}
throw e;
}
}
export class AbortError extends Error {
constructor(message: string) {
super(message);
}
}

View file

@ -1,84 +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
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
/* eslint-disable no-console */
import { argv } from 'yargs';
import { isAxiosError } from './call_kibana';
import { createKibanaUserRole, AbortError } from './create_kibana_user_role';
const esUserName = (argv.username as string) || 'elastic';
const esPassword = argv.password as string | undefined;
const kibanaBaseUrl = argv.kibanaUrl as string | undefined;
const kibanaRoleSuffix = argv.roleSuffix as string | undefined;
if (!esPassword) {
throw new Error(
'Please specify credentials for elasticsearch: `--username elastic --password abcd` '
);
}
if (!kibanaBaseUrl) {
throw new Error(
'Please specify the url for Kibana: `--kibana-url http://localhost:5601` '
);
}
if (
!kibanaBaseUrl.startsWith('https://') &&
!kibanaBaseUrl.startsWith('http://')
) {
throw new Error(
'Kibana url must be prefixed with http(s):// `--kibana-url http://localhost:5601`'
);
}
if (!kibanaRoleSuffix) {
throw new Error(
'Please specify a unique suffix that will be added to your roles with `--role-suffix <suffix>` '
);
}
console.log({
kibanaRoleSuffix,
esUserName,
esPassword,
kibanaBaseUrl,
});
createKibanaUserRole({
kibana: {
roleSuffix: kibanaRoleSuffix,
hostname: kibanaBaseUrl,
},
elasticsearch: {
username: esUserName,
password: esPassword,
},
}).catch((e) => {
if (e instanceof AbortError) {
console.error(e.message);
} else if (isAxiosError(e)) {
console.error(
`${e.config.method?.toUpperCase() || 'GET'} ${e.config.url} (Code: ${
e.response?.status
})`
);
if (e.response) {
console.error(
JSON.stringify(
{ request: e.config, response: e.response.data },
null,
2
)
);
}
} else {
console.error(e);
}
});

View file

@ -5751,8 +5751,6 @@
"xpack.apm.serviceOverview.transactionsTableColumnImpact": "インパクト",
"xpack.apm.serviceOverview.transactionsTableColumnName": "名前",
"xpack.apm.serviceOverview.transactionsTableColumnThroughput": "スループット",
"xpack.apm.serviceOverview.transactionsTableLinkText": "トランザクションを表示",
"xpack.apm.serviceOverview.transactionsTableTitle": "トランザクション",
"xpack.apm.serviceProfiling.valueTypeLabel.allocObjects": "Alloc. objects",
"xpack.apm.serviceProfiling.valueTypeLabel.allocSpace": "Alloc. space",
"xpack.apm.serviceProfiling.valueTypeLabel.cpuTime": "On-CPU",
@ -5941,9 +5939,6 @@
"xpack.apm.transactionActionMenu.viewInUptime": "ステータス",
"xpack.apm.transactionActionMenu.viewSampleDocumentLinkLabel": "サンプルドキュメントを表示",
"xpack.apm.transactionBreakdown.chartTitle": "スパンタイプ別時間",
"xpack.apm.transactionCardinalityWarning.body": "一意のトランザクション名の数が構成された値{bucketSize}を超えています。エージェントを再構成し、類似したトランザクションをグループ化するか、{codeBlock}の値を増やしてください。",
"xpack.apm.transactionCardinalityWarning.docsLink": "詳細はドキュメントをご覧ください",
"xpack.apm.transactionCardinalityWarning.title": "このビューには、報告されたトランザクションのサブセットが表示されます。",
"xpack.apm.transactionDetails.noTraceParentButtonTooltip": "トレースの親が見つかりませんでした",
"xpack.apm.transactionDetails.percentOfTraceLabelExplanation": "{parentType, select, transaction {トランザクション} trace {トレース} }の割合が100%を超えています。これは、この{childType, select, span {スパン} transaction {トランザクション} }がルートトランザクションよりも時間がかかるためです。",
"xpack.apm.transactionDetails.requestMethodLabel": "リクエストメソッド",

View file

@ -5779,8 +5779,6 @@
"xpack.apm.serviceOverview.transactionsTableColumnImpact": "影响",
"xpack.apm.serviceOverview.transactionsTableColumnName": "名称",
"xpack.apm.serviceOverview.transactionsTableColumnThroughput": "吞吐量",
"xpack.apm.serviceOverview.transactionsTableLinkText": "查看事务",
"xpack.apm.serviceOverview.transactionsTableTitle": "事务",
"xpack.apm.serviceProfiling.valueTypeLabel.allocObjects": "分配的对象",
"xpack.apm.serviceProfiling.valueTypeLabel.allocSpace": "分配的空间",
"xpack.apm.serviceProfiling.valueTypeLabel.cpuTime": "CPU 上",
@ -5971,9 +5969,6 @@
"xpack.apm.transactionActionMenu.viewInUptime": "状态",
"xpack.apm.transactionActionMenu.viewSampleDocumentLinkLabel": "查看样例文档",
"xpack.apm.transactionBreakdown.chartTitle": "跨度类型花费的时间",
"xpack.apm.transactionCardinalityWarning.body": "唯一事务名称的数目超过 {bucketSize} 的已配置值。尝试重新配置您的代理以对类似的事务分组或增大 {codeBlock} 的值",
"xpack.apm.transactionCardinalityWarning.docsLink": "在文档中了解详情",
"xpack.apm.transactionCardinalityWarning.title": "此视图显示已报告事务的子集。",
"xpack.apm.transactionDetails.errorCount": "{errorCount, number} 个 {errorCount, plural, other {错误}}",
"xpack.apm.transactionDetails.errorsOverviewLinkTooltip": "{errorCount, plural, one {查看 1 个相关错误} other {查看 # 个相关错误}}",
"xpack.apm.transactionDetails.noTraceParentButtonTooltip": "找不到上级追溯",