[8.x] [Inventory] Adding initial e2e structure (#196560) (#196808)

# Backport

This will backport the following commits from `main` to `8.x`:
- [[Inventory] Adding initial e2e structure
(#196560)](https://github.com/elastic/kibana/pull/196560)

<!--- Backport version: 8.9.8 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sqren/backport)

<!--BACKPORT [{"author":{"name":"Cauê
Marcondes","email":"55978943+cauemarcondes@users.noreply.github.com"},"sourceCommit":{"committedDate":"2024-10-17T13:50:11Z","message":"[Inventory]
Adding initial e2e structure (#196560)\n\ncloses
https://github.com/elastic/kibana/issues/193992\r\n\r\nHow to open
cypress dashboard locally:\r\n```\r\nnode
x-pack/plugins/observability_solution/inventory/scripts/test/e2e.js
--open \r\n```\r\n\r\nHow to run cypress tests:\r\n```\r\nnode
x-pack/plugins/observability_solution/inventory/scripts/test/e2e.js\r\n```\r\n\r\nHow
to run cypress tests multiple times:\r\n```\r\nnode
x-pack/plugins/observability_solution/inventory/scripts/test/e2e.js
--server\r\nnode
x-pack/plugins/observability_solution/inventory/scripts/test/e2e.js
--runner --times=X\r\n```\r\n\r\n---------\r\n\r\nCo-authored-by:
kibanamachine
<42973632+kibanamachine@users.noreply.github.com>","sha":"3bfa7c00181599541c924d36b593205fd5d9fed4","branchLabelMapping":{"^v9.0.0$":"main","^v8.17.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","v9.0.0","backport:prev-minor","ci:project-deploy-observability","Team:obs-ux-infra_services","v8.16.0"],"number":196560,"url":"https://github.com/elastic/kibana/pull/196560","mergeCommit":{"message":"[Inventory]
Adding initial e2e structure (#196560)\n\ncloses
https://github.com/elastic/kibana/issues/193992\r\n\r\nHow to open
cypress dashboard locally:\r\n```\r\nnode
x-pack/plugins/observability_solution/inventory/scripts/test/e2e.js
--open \r\n```\r\n\r\nHow to run cypress tests:\r\n```\r\nnode
x-pack/plugins/observability_solution/inventory/scripts/test/e2e.js\r\n```\r\n\r\nHow
to run cypress tests multiple times:\r\n```\r\nnode
x-pack/plugins/observability_solution/inventory/scripts/test/e2e.js
--server\r\nnode
x-pack/plugins/observability_solution/inventory/scripts/test/e2e.js
--runner --times=X\r\n```\r\n\r\n---------\r\n\r\nCo-authored-by:
kibanamachine
<42973632+kibanamachine@users.noreply.github.com>","sha":"3bfa7c00181599541c924d36b593205fd5d9fed4"}},"sourceBranch":"main","suggestedTargetBranches":["8.16"],"targetPullRequestStates":[{"branch":"main","label":"v9.0.0","labelRegex":"^v9.0.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/196560","number":196560,"mergeCommit":{"message":"[Inventory]
Adding initial e2e structure (#196560)\n\ncloses
https://github.com/elastic/kibana/issues/193992\r\n\r\nHow to open
cypress dashboard locally:\r\n```\r\nnode
x-pack/plugins/observability_solution/inventory/scripts/test/e2e.js
--open \r\n```\r\n\r\nHow to run cypress tests:\r\n```\r\nnode
x-pack/plugins/observability_solution/inventory/scripts/test/e2e.js\r\n```\r\n\r\nHow
to run cypress tests multiple times:\r\n```\r\nnode
x-pack/plugins/observability_solution/inventory/scripts/test/e2e.js
--server\r\nnode
x-pack/plugins/observability_solution/inventory/scripts/test/e2e.js
--runner --times=X\r\n```\r\n\r\n---------\r\n\r\nCo-authored-by:
kibanamachine
<42973632+kibanamachine@users.noreply.github.com>","sha":"3bfa7c00181599541c924d36b593205fd5d9fed4"}},{"branch":"8.16","label":"v8.16.0","labelRegex":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"}]}]
BACKPORT-->

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Cauê Marcondes 2024-10-18 15:02:09 +01:00 committed by GitHub
parent 1434d5aa0c
commit 0641343906
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
43 changed files with 2940 additions and 63470 deletions

View file

@ -3,9 +3,10 @@ disabled:
- x-pack/plugins/observability_solution/observability_onboarding/e2e/ftr_config_open.ts
- x-pack/plugins/observability_solution/observability_onboarding/e2e/ftr_config_runner.ts
- x-pack/plugins/observability_solution/observability_onboarding/e2e/ftr_config.ts
- x-pack/plugins/observability_solution/apm/ftr_e2e/ftr_config_open.ts
- x-pack/plugins/observability_solution/apm/ftr_e2e/ftr_config_run.ts
- x-pack/plugins/observability_solution/apm/ftr_e2e/ftr_config.ts
- x-pack/plugins/observability_solution/inventory/e2e/ftr_config_run.ts
- x-pack/plugins/observability_solution/inventory/e2e/ftr_config.ts
- x-pack/plugins/observability_solution/profiling/e2e/ftr_config_open.ts
- x-pack/plugins/observability_solution/profiling/e2e/ftr_config_runner.ts
- x-pack/plugins/observability_solution/profiling/e2e/ftr_config.ts

View file

@ -23,7 +23,7 @@
{
"key": "cypress/security_serverless_explore",
"name": "[Serverless] Security Solution Explore - Cypress"
},
},
{
"key": "cypress/security_solution_rule_management",
"name": "Security Solution Rule Management - Cypress"
@ -87,6 +87,10 @@
{
"key": "cypress/apm_cypress",
"name": "APM - Cypress"
},
{
"key": "cypress/inventory_cypress",
"name": "Inventory - Cypress"
}
]
}
}

View file

@ -80,3 +80,20 @@ steps:
limit: 3
- exit_status: '*'
limit: 1
- command: .buildkite/scripts/steps/functional/inventory_cypress.sh
label: 'Inventory Cypress Tests'
agents:
image: family/kibana-ubuntu-2004
imageProject: elastic-images-prod
provider: gcp
machineType: n2-standard-4
preemptible: true
depends_on: build
timeout_in_minutes: 120
retry:
automatic:
- exit_status: '-1'
limit: 3
- exit_status: '*'
limit: 1

View file

@ -0,0 +1,17 @@
steps:
- command: .buildkite/scripts/steps/functional/inventory_cypress.sh
label: 'Inventory Cypress Tests'
agents:
machineType: n2-standard-4
preemptible: true
depends_on:
- build
- quick_checks
timeout_in_minutes: 120
parallelism: 1
retry:
automatic:
- exit_status: '-1'
limit: 3
- exit_status: '*'
limit: 1

View file

@ -77,6 +77,16 @@ const getPipeline = (filename: string, removeSteps = true) => {
pipeline.push(getPipeline('.buildkite/pipelines/pull_request/apm_cypress.yml'));
}
if (
(await doAnyChangesMatch([
/^x-pack\/plugins\/observability_solution\/inventory/,
/^packages\/kbn-apm-synthtrace/,
])) ||
GITHUB_PR_LABELS.includes('ci:all-cypress-suites')
) {
pipeline.push(getPipeline('.buildkite/pipelines/pull_request/inventory_cypress.yml'));
}
if (
(await doAnyChangesMatch([
/^x-pack\/plugins\/observability_solution\/observability_onboarding/,

View file

@ -0,0 +1,17 @@
#!/usr/bin/env bash
set -euo pipefail
source .buildkite/scripts/common/util.sh
.buildkite/scripts/bootstrap.sh
.buildkite/scripts/download_build_artifacts.sh
export JOB=kibana-inventory-onboarding-cypress
echo "--- Observability Inventory Cypress Tests"
cd "$XPACK_DIR"
node plugins/observability_solution/inventory/scripts/test/e2e.js \
--kibana-install-dir "$KIBANA_BUILD_LOCATION" \

1937
.github/CODEOWNERS vendored Normal file

File diff suppressed because it is too large Load diff

View file

@ -1443,6 +1443,7 @@
"@kbn/get-repo-files": "link:packages/kbn-get-repo-files",
"@kbn/import-locator": "link:packages/kbn-import-locator",
"@kbn/import-resolver": "link:packages/kbn-import-resolver",
"@kbn/inventory-e2e": "link:x-pack/plugins/observability_solution/inventory/e2e",
"@kbn/jest-serializers": "link:packages/kbn-jest-serializers",
"@kbn/journeys": "link:packages/kbn-journeys",
"@kbn/json-ast": "link:packages/kbn-json-ast",

View file

@ -15,7 +15,8 @@ class ContainerEntity extends Serializable<EntityFields> {
super({
...fields,
'entity.type': 'container',
'entity.definitionId': 'latest',
'entity.definitionId': 'builtin_containers_from_ecs_data',
'entity.identityFields': ['container.id'],
});
}
}
@ -23,21 +24,19 @@ class ContainerEntity extends Serializable<EntityFields> {
export function containerEntity({
agentName,
dataStreamType,
dataStreamDataset,
containerId,
entityId,
}: {
agentName: string[];
dataStreamType: EntityDataStreamType[];
dataStreamDataset: string;
containerId: string;
entityId: string;
}) {
return new ContainerEntity({
'source_data_stream.type': dataStreamType,
'source_data_stream.dataset': dataStreamDataset,
'agent.name': agentName,
'container.id': containerId,
'entity.displayName': containerId,
'entity.id': entityId,
});
}

View file

@ -15,7 +15,8 @@ class HostEntity extends Serializable<EntityFields> {
super({
...fields,
'entity.type': 'host',
'entity.definitionId': 'latest',
'entity.definitionId': 'builtin_hosts_from_ecs_data',
'entity.identityFields': ['host.name'],
});
}
}
@ -23,21 +24,19 @@ class HostEntity extends Serializable<EntityFields> {
export function hostEntity({
agentName,
dataStreamType,
dataStreamDataset,
hostName,
entityId,
}: {
agentName: string[];
dataStreamType: EntityDataStreamType[];
dataStreamDataset: string;
hostName: string;
entityId: string;
}) {
return new HostEntity({
'source_data_stream.type': dataStreamType,
'source_data_stream.dataset': dataStreamDataset,
'agent.name': agentName,
'host.name': hostName,
'entity.displayName': hostName,
'entity.id': entityId,
});
}

View file

@ -15,7 +15,8 @@ class ServiceEntity extends Serializable<EntityFields> {
super({
...fields,
'entity.type': 'service',
'entity.definitionId': 'latest',
'entity.definitionId': 'builtin_services_from_ecs_data',
'entity.identityFields': ['service.name'],
});
}
}
@ -35,6 +36,7 @@ export function serviceEntity({
}) {
return new ServiceEntity({
'service.name': serviceName,
'entity.displayName': serviceName,
'service.environment': environment,
'source_data_stream.type': dataStreamType,
'agent.name': agentName,

View file

@ -6,8 +6,6 @@
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { randomInt } from 'crypto';
import { Fields } from '../entity';
import { Serializable } from '../serializable';
@ -180,3 +178,12 @@ export const log = {
create,
createMinimal,
};
function randomInt(min: number, max: number) {
if (min > max) {
throw new Error('Min value must be less than or equal to max value.');
}
const random = Math.floor(Math.random() * (max - min + 1)) + min;
return random;
}

View file

@ -16,6 +16,7 @@ export { InfraSynthtraceKibanaClient } from './src/lib/infra/infra_synthtrace_ki
export { MonitoringSynthtraceEsClient } from './src/lib/monitoring/monitoring_synthtrace_es_client';
export { LogsSynthtraceEsClient } from './src/lib/logs/logs_synthtrace_es_client';
export { EntitiesSynthtraceEsClient } from './src/lib/entities/entities_synthtrace_es_client';
export { EntitiesSynthtraceKibanaClient } from './src/lib/entities/entities_synthtrace_kibana_client';
export { SyntheticsSynthtraceEsClient } from './src/lib/synthetics/synthetics_synthtrace_es_client';
export {
addObserverVersionTransform,

View file

@ -18,7 +18,7 @@ import {
import { Logger } from '../lib/utils/create_logger';
import { ScenarioReturnType } from '../lib/utils/with_client';
import { RunOptions } from './utils/parse_run_cli_flags';
import { EntitiesSynthtraceKibanaClient } from '../lib/apm/client/entities_synthtrace_kibana_client';
import { EntitiesSynthtraceKibanaClient } from '../lib/entities/entities_synthtrace_kibana_client';
interface EsClients {
apmEsClient: ApmSynthtraceEsClient;

View file

@ -7,7 +7,7 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { EntitiesSynthtraceKibanaClient } from '../../lib/apm/client/entities_synthtrace_kibana_client';
import { EntitiesSynthtraceKibanaClient } from '../../lib/entities/entities_synthtrace_kibana_client';
import { Logger } from '../../lib/utils/create_logger';
export function getEntitiesKibanaClient({ target, logger }: { target: string; logger: Logger }) {

View file

@ -17,6 +17,10 @@ import { Logger } from '../utils/create_logger';
export type EntitiesSynthtraceEsClientOptions = Omit<SynthtraceEsClientOptions, 'pipeline'>;
interface Pipeline {
includeSerialization?: boolean;
}
export class EntitiesSynthtraceEsClient extends SynthtraceEsClient<EntityFields> {
constructor(options: { client: Client; logger: Logger } & EntitiesSynthtraceEsClientOptions) {
super({
@ -25,13 +29,20 @@ export class EntitiesSynthtraceEsClient extends SynthtraceEsClient<EntityFields>
});
this.indices = ['.entities.v1.latest.builtin*'];
}
getDefaultPipeline({ includeSerialization }: Pipeline = { includeSerialization: true }) {
return entitiesPipeline({ includeSerialization });
}
}
function entitiesPipeline() {
function entitiesPipeline({ includeSerialization }: Pipeline = { includeSerialization: true }) {
return (base: Readable) => {
const serializationTransform = includeSerialization ? [getSerializeTransform()] : [];
return pipeline(
// @ts-expect-error Some weird stuff here with the type definition for pipeline. We have tests!
base,
getSerializeTransform(),
...serializationTransform,
lastSeenTimestampTransform(),
getRoutingTransform(),
getDedotTransform(),

View file

@ -8,9 +8,9 @@
*/
import fetch from 'node-fetch';
import { Logger } from '../../utils/create_logger';
import { kibanaHeaders } from '../../shared/client_headers';
import { getFetchAgent } from '../../../cli/utils/ssl';
import { Logger } from '../utils/create_logger';
import { kibanaHeaders } from '../shared/client_headers';
import { getFetchAgent } from '../../cli/utils/ssl';
interface EntityDefinitionResponse {
definitions: Array<{ type: string; state: { installed: boolean; running: boolean } }>;

View file

@ -23,6 +23,10 @@ export const LogsCustom = 'logs@custom';
export type LogsSynthtraceEsClientOptions = Omit<SynthtraceEsClientOptions, 'pipeline'>;
interface Pipeline {
includeSerialization?: boolean;
}
export class LogsSynthtraceEsClient extends SynthtraceEsClient<LogDocument> {
constructor(options: { client: Client; logger: Logger } & LogsSynthtraceEsClientOptions) {
super({
@ -105,13 +109,22 @@ export class LogsSynthtraceEsClient extends SynthtraceEsClient<LogDocument> {
this.logger.error(`Custom pipeline creation failed: ${LogsCustom} - ${err.message}`);
}
}
getDefaultPipeline({ includeSerialization }: Pipeline = { includeSerialization: true }) {
return logsPipeline({ includeSerialization });
}
}
function logsPipeline() {
function logsPipeline({ includeSerialization }: Pipeline = { includeSerialization: true }) {
return (base: Readable) => {
const serializationTransform = includeSerialization
? [getSerializeTransform<LogDocument>()]
: [];
return pipeline(
// @ts-expect-error Some weird stuff here with the type definition for pipeline. We have tests!
base,
getSerializeTransform<LogDocument>(),
...serializationTransform,
getRoutingTransform('logs'),
(err: unknown) => {
if (err) {

View file

@ -1062,6 +1062,8 @@
"@kbn/interactive-setup-test-endpoints-plugin/*": ["test/interactive_setup_api_integration/plugins/test_endpoints/*"],
"@kbn/interpreter": ["packages/kbn-interpreter"],
"@kbn/interpreter/*": ["packages/kbn-interpreter/*"],
"@kbn/inventory-e2e": ["x-pack/plugins/observability_solution/inventory/e2e"],
"@kbn/inventory-e2e/*": ["x-pack/plugins/observability_solution/inventory/e2e/*"],
"@kbn/inventory-plugin": ["x-pack/plugins/observability_solution/inventory"],
"@kbn/inventory-plugin/*": ["x-pack/plugins/observability_solution/inventory/*"],
"@kbn/investigate-app-plugin": ["x-pack/plugins/observability_solution/investigate_app"],

View file

@ -1,3 +1,21 @@
# Inventory
Home of the Inventory plugin, which renders the... _inventory_.
# Running e2e (Cypress) tests
How to open cypress dashboard locally:
```
node x-pack/plugins/observability_solution/inventory/scripts/test/e2e.js --open
```
How to run cypress tests:
```
node x-pack/plugins/observability_solution/inventory/scripts/test/e2e.js
```
How to run cypress tests multiple times:
```
node x-pack/plugins/observability_solution/inventory/scripts/test/e2e.js --server
node x-pack/plugins/observability_solution/inventory/scripts/test/e2e.js --runner --times=X
```

View file

@ -0,0 +1 @@
TBD

View file

@ -0,0 +1,2 @@
package_paths:
- /packages/package-storage

View file

@ -0,0 +1,35 @@
/*
* 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 { defineCypressConfig } from '@kbn/cypress-config';
import { setupNodeEvents } from './setup_cypress_node_events';
export default defineCypressConfig({
projectId: 'omwh6f',
fileServerFolder: './cypress',
fixturesFolder: './cypress/fixtures',
screenshotsFolder: './cypress/screenshots',
videosFolder: './cypress/videos',
requestTimeout: 10000,
responseTimeout: 40000,
defaultCommandTimeout: 30000,
execTimeout: 120000,
pageLoadTimeout: 120000,
viewportHeight: 1800,
viewportWidth: 1440,
video: true,
screenshotOnRunFailure: true,
retries: {
runMode: 1,
},
e2e: {
setupNodeEvents,
baseUrl: 'http://localhost:5601',
supportFile: './cypress/support/e2e.ts',
specPattern: './cypress/e2e/**/*.cy.{js,jsx,ts,tsx}',
},
});

View file

@ -0,0 +1,2 @@
/videos/*
/screenshots/*

View file

@ -0,0 +1,112 @@
/*
* 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 { apm, entities, log, timerange } from '@kbn/apm-synthtrace-client';
import { generateLongIdWithSeed } from '@kbn/apm-synthtrace-client/src/lib/utils/generate_id';
const SYNTH_NODE_TRACES_LOGS_ENTITY_ID = generateLongIdWithSeed('service');
const HOST_SERVER_1_LOGS_ENTITY_ID = generateLongIdWithSeed('host');
const CONTAINER_ID_METRICS_ENTITY_ID = generateLongIdWithSeed('container');
const SYNTH_NODE_TRACE_LOGS = 'synth-node-trace-logs';
const HOST_NAME = 'server1';
const CONTAINER_ID = 'foo';
const ENVIRONMENT = 'test';
export function generateEntities({ from, to }: { from: number; to: number }) {
const serviceSynthNodeTracesLogs = entities.serviceEntity({
serviceName: SYNTH_NODE_TRACE_LOGS,
agentName: ['nodejs'],
dataStreamType: ['traces', 'logs'],
environment: ENVIRONMENT,
entityId: SYNTH_NODE_TRACES_LOGS_ENTITY_ID,
});
const hostServer1Logs = entities.hostEntity({
hostName: HOST_NAME,
agentName: ['nodejs'],
dataStreamType: ['logs'],
entityId: HOST_SERVER_1_LOGS_ENTITY_ID,
});
const containerMetrics = entities.containerEntity({
containerId: CONTAINER_ID,
agentName: ['filebeat'],
dataStreamType: ['metrics'],
entityId: CONTAINER_ID_METRICS_ENTITY_ID,
});
const range = timerange(from, to);
return range
.interval('1m')
.rate(1)
.generator((timestamp) => {
return [
serviceSynthNodeTracesLogs.timestamp(timestamp),
hostServer1Logs.timestamp(timestamp),
containerMetrics.timestamp(timestamp),
];
});
}
export function generateTraces({ from, to }: { from: number; to: number }) {
const synthNodeTraceLogs = apm
.service({
name: SYNTH_NODE_TRACE_LOGS,
environment: ENVIRONMENT,
agentName: 'nodejs',
})
.instance('instance_1');
const range = timerange(from, to);
return range
.interval('1m')
.rate(1)
.generator((timestamp) => {
return [
synthNodeTraceLogs
.transaction({ transactionName: 't1' })
.timestamp(timestamp)
.duration(1000)
.success(),
];
});
}
const MESSAGE_LOG_LEVELS = [
{ message: 'A simple log', level: 'info' },
{ message: 'Yet another debug log', level: 'debug' },
{ message: 'Error with certificate: "ca_trusted_fingerprint"', level: 'error' },
];
export function generateLogs({ from, to }: { from: number; to: number }) {
const range = timerange(from, to);
return range
.interval('1m')
.rate(1)
.generator((timestamp) => {
return Array(3)
.fill(0)
.map(() => {
const index = Math.floor(Math.random() * 3);
const logMessage = MESSAGE_LOG_LEVELS[index];
return log
.create({ isLogsDb: false })
.service(SYNTH_NODE_TRACE_LOGS)
.message(logMessage.message)
.logLevel(logMessage.level)
.setGeoLocation([1])
.setHostIp('223.72.43.22')
.defaults({
'agent.name': 'nodejs',
})
.timestamp(timestamp);
});
});
}

View file

@ -0,0 +1,158 @@
/*
* 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 { apmSynthtrace, entitiesSynthtrace, logsSynthtrace } from '../../synthtrace';
import { generateEntities, generateLogs, generateTraces } from './generate_data';
const start = '2024-10-16T00:00:00.000Z';
const end = '2024-10-16T00:15:00.000Z';
describe('Home page', () => {
beforeEach(() => {
cy.loginAsSuperUser();
});
describe('When EEM is disabled', () => {
it('Shows no data screen', () => {
cy.visitKibana('/app/inventory');
cy.contains('See everything you have in one place');
cy.getByTestSubj('inventoryInventoryPageTemplateFilledButton').should('exist');
});
});
describe('When EEM is enabled', () => {
describe('When there is no entities', () => {
it('Shows inventory page with empty message', () => {
cy.intercept('GET', '/internal/entities/managed/enablement', {
fixture: 'eem_enabled.json',
}).as('getEEMStatus');
cy.visitKibana('/app/inventory');
cy.wait('@getEEMStatus');
cy.contains('Inventory');
cy.contains('Tell us what you think!');
cy.contains('Trying for the first time?');
cy.contains('No entities available');
cy.getByTestSubj('addDataButton').should('exist');
cy.getByTestSubj('associateServiceLogsButton').should('exist');
});
});
describe('When there is entities and signal data', () => {
before(() => {
entitiesSynthtrace.index(
generateEntities({ from: new Date(start).getTime(), to: new Date(end).getTime() })
);
apmSynthtrace.index(
generateTraces({ from: new Date(start).getTime(), to: new Date(end).getTime() })
);
logsSynthtrace.index(
generateLogs({ from: new Date(start).getTime(), to: new Date(end).getTime() })
);
});
after(() => {
entitiesSynthtrace.clean();
apmSynthtrace.clean();
logsSynthtrace.clean();
});
it('Shows inventory page with entities', () => {
cy.intercept('GET', '/internal/entities/managed/enablement', {
fixture: 'eem_enabled.json',
}).as('getEEMStatus');
cy.visitKibana('/app/inventory');
cy.wait('@getEEMStatus');
cy.contains('server1');
cy.contains('Host');
cy.contains('synth-node-trace-logs');
cy.contains('Service');
cy.contains('foo');
cy.contains('Container');
});
it('Navigates to apm when clicking on a service type entity', () => {
cy.intercept('GET', '/internal/entities/managed/enablement', {
fixture: 'eem_enabled.json',
}).as('getEEMStatus');
cy.visitKibana('/app/inventory');
cy.wait('@getEEMStatus');
cy.contains('synth-node-trace-logs').click();
cy.url().should('include', '/app/apm/services/synth-node-trace-logs/overview');
});
it('Navigates to hosts when clicking on a host type entity', () => {
cy.intercept('GET', '/internal/entities/managed/enablement', {
fixture: 'eem_enabled.json',
}).as('getEEMStatus');
cy.visitKibana('/app/inventory');
cy.wait('@getEEMStatus');
cy.contains('server1').click();
cy.url().should('include', '/app/metrics/detail/host/server1');
});
it('Navigates to infra when clicking on a container type entity', () => {
cy.intercept('GET', '/internal/entities/managed/enablement', {
fixture: 'eem_enabled.json',
}).as('getEEMStatus');
cy.visitKibana('/app/inventory');
cy.wait('@getEEMStatus');
cy.contains('foo').click();
cy.url().should('include', '/app/metrics/detail/container/foo');
});
it('Filters entities by service type', () => {
cy.intercept('GET', '/internal/entities/managed/enablement', {
fixture: 'eem_enabled.json',
}).as('getEEMStatus');
cy.intercept('GET', '/internal/inventory/entities*').as('getEntitites');
cy.visitKibana('/app/inventory');
cy.wait('@getEEMStatus');
cy.getByTestSubj('entityTypesFilterComboBox')
.click()
.getByTestSubj('entityTypesFilterserviceOption')
.click();
cy.wait('@getEntitites');
cy.get('server1').should('not.exist');
cy.contains('synth-node-trace-logs');
cy.get('foo').should('not.exist');
});
it('Filters entities by host type', () => {
cy.intercept('GET', '/internal/entities/managed/enablement', {
fixture: 'eem_enabled.json',
}).as('getEEMStatus');
cy.intercept('GET', '/internal/inventory/entities*').as('getEntitites');
cy.visitKibana('/app/inventory');
cy.wait('@getEEMStatus');
cy.getByTestSubj('entityTypesFilterComboBox')
.click()
.getByTestSubj('entityTypesFilterhostOption')
.click();
cy.wait('@getEntitites');
cy.contains('server1');
cy.get('synth-node-trace-logs').should('not.exist');
cy.get('foo').should('not.exist');
});
it('Filters entities by container type', () => {
cy.intercept('GET', '/internal/entities/managed/enablement', {
fixture: 'eem_enabled.json',
}).as('getEEMStatus');
cy.intercept('GET', '/internal/inventory/entities*').as('getEntitites');
cy.visitKibana('/app/inventory');
cy.wait('@getEEMStatus');
cy.getByTestSubj('entityTypesFilterComboBox')
.click()
.getByTestSubj('entityTypesFiltercontainerOption')
.click();
cy.wait('@getEntitites');
cy.get('server1').should('not.exist');
cy.get('synth-node-trace-logs').should('not.exist');
cy.contains('foo');
});
});
});
});

View file

@ -0,0 +1,3 @@
{
"enabled": true
}

View file

@ -0,0 +1,49 @@
/*
* 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 '@frsource/cypress-plugin-visual-regression-diff';
import 'cypress-axe';
import 'cypress-real-events/support';
Cypress.Commands.add('getByTestSubj', (selector: string) => {
return cy.get(`[data-test-subj="${selector}"]`);
});
Cypress.Commands.add('visitKibana', (url: string) => {
cy.visit(url);
cy.getByTestSubj('kbnLoadingMessage').should('exist');
cy.getByTestSubj('kbnLoadingMessage').should('not.exist', {
timeout: 50000,
});
});
Cypress.Commands.add('loginAsSuperUser', () => {
return cy.loginAs({ username: 'elastic', password: 'changeme' });
});
Cypress.Commands.add(
'loginAs',
({ username, password }: { username: string; password: string }) => {
const kibanaUrl = Cypress.env('KIBANA_URL');
cy.log(`Logging in as ${username} on ${kibanaUrl}`);
cy.visit('/');
cy.request({
log: true,
method: 'POST',
url: `${kibanaUrl}/internal/security/login`,
body: {
providerType: 'basic',
providerName: 'basic',
currentURL: `${kibanaUrl}/login`,
params: { username, password },
},
headers: {
'kbn-xsrf': 'e2e_test',
},
});
cy.visit('/');
}
);

View file

@ -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
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
Cypress.on('uncaught:exception', (err, runnable) => {
return false;
});
import './commands';

View file

@ -0,0 +1,18 @@
/*
* 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.
*/
declare namespace Cypress {
interface Chainable {
loginAsSuperUser(): Cypress.Chainable<Cypress.Response<any>>;
loginAs(params: {
username: string;
password: string;
}): Cypress.Chainable<Cypress.Response<any>>;
getByTestSubj(selector: string): Chainable<JQuery<Element>>;
visitKibana(url: string): void;
}
}

View file

@ -0,0 +1,103 @@
/*
* 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 {
EntitiesSynthtraceKibanaClient,
createLogger,
LogLevel,
ApmSynthtraceKibanaClient,
} from '@kbn/apm-synthtrace';
import cypress from 'cypress';
import path from 'path';
import Url from 'url';
import { FtrProviderContext } from './ftr_provider_context';
export async function cypressTestRunner({ getService }: FtrProviderContext) {
const config = getService('config');
const username = config.get('servers.elasticsearch.username');
const password = config.get('servers.elasticsearch.password');
const kibanaUrl = Url.format({
protocol: config.get('servers.kibana.protocol'),
hostname: config.get('servers.kibana.hostname'),
port: config.get('servers.kibana.port'),
auth: `${username}:${password}`,
});
const esNode = Url.format({
protocol: config.get('servers.elasticsearch.protocol'),
port: config.get('servers.elasticsearch.port'),
hostname: config.get('servers.elasticsearch.hostname'),
auth: `${username}:${password}`,
});
const esRequestTimeout = config.get('timeouts.esRequestTimeout');
const apmKibanaClient = new ApmSynthtraceKibanaClient({
logger: createLogger(LogLevel.info),
target: kibanaUrl,
});
const packageVersion = await apmKibanaClient.fetchLatestApmPackageVersion();
const entitiesKibanaClient = new EntitiesSynthtraceKibanaClient({
logger: createLogger(LogLevel.info),
target: kibanaUrl,
});
await Promise.all([
apmKibanaClient.installApmPackage(packageVersion),
entitiesKibanaClient.installEntityIndexPatterns(),
]);
const kibanaUrlWithoutAuth = Url.format({
protocol: config.get('servers.kibana.protocol'),
hostname: config.get('servers.kibana.hostname'),
port: config.get('servers.kibana.port'),
});
const cypressProjectPath = path.join(__dirname);
const { open, ...cypressCliArgs } = getCypressCliArgs();
const cypressExecution = open ? cypress.open : cypress.run;
const res = await cypressExecution({
...cypressCliArgs,
project: cypressProjectPath,
browser: 'electron',
config: {
e2e: {
baseUrl: kibanaUrlWithoutAuth,
},
},
env: {
KIBANA_URL: kibanaUrlWithoutAuth,
APM_PACKAGE_VERSION: packageVersion,
ES_NODE: esNode,
ES_REQUEST_TIMEOUT: esRequestTimeout,
TEST_CLOUD: process.env.TEST_CLOUD,
},
});
return res;
}
function getCypressCliArgs(): Record<string, unknown> {
if (!process.env.CYPRESS_CLI_ARGS) {
return {};
}
const { $0, _, ...cypressCliArgs } = JSON.parse(process.env.CYPRESS_CLI_ARGS) as Record<
string,
unknown
>;
const spec =
typeof cypressCliArgs.spec === 'string' && !cypressCliArgs.spec.includes('**')
? `**/${cypressCliArgs.spec}*`
: cypressCliArgs.spec;
return { ...cypressCliArgs, spec };
}

View file

@ -0,0 +1,66 @@
/*
* 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 { FtrConfigProviderContext } from '@kbn/test';
import { CA_CERT_PATH } from '@kbn/dev-utils';
import { commonFunctionalServices } from '@kbn/ftr-common-functional-services';
import { commonFunctionalUIServices } from '@kbn/ftr-common-functional-ui-services';
import { cypressTestRunner } from './cypress_test_runner';
import { FtrProviderContext } from './ftr_provider_context';
async function ftrConfig({ readConfigFile }: FtrConfigProviderContext) {
const kibanaCommonTestsConfig = await readConfigFile(
require.resolve('@kbn/test-suites-src/common/config')
);
const xpackFunctionalTestsConfig = await readConfigFile(
require.resolve('@kbn/test-suites-xpack/functional/config.base')
);
return {
...kibanaCommonTestsConfig.getAll(),
services: {
...commonFunctionalServices,
...commonFunctionalUIServices,
},
esTestCluster: {
...xpackFunctionalTestsConfig.get('esTestCluster'),
serverArgs: [
...xpackFunctionalTestsConfig.get('esTestCluster.serverArgs'),
// define custom es server here
],
},
kbnTestServer: {
...xpackFunctionalTestsConfig.get('kbnTestServer'),
serverArgs: [
...xpackFunctionalTestsConfig.get('kbnTestServer.serverArgs'),
'--home.disableWelcomeScreen=true',
'--csp.strict=false',
'--csp.warnLegacyBrowsers=false',
// define custom kibana server args here
`--elasticsearch.ssl.certificateAuthorities=${CA_CERT_PATH}`,
],
},
testRunner: async (ftrProviderContext: FtrProviderContext) => {
const result = await cypressTestRunner(ftrProviderContext);
// set exit code explicitly if at least one Cypress test fails
if (
result &&
((result as CypressCommandLine.CypressFailedRunResult)?.status === 'failed' ||
(result as CypressCommandLine.CypressRunResult)?.totalFailed)
) {
process.exitCode = 1;
}
},
};
}
// eslint-disable-next-line import/no-default-export
export default ftrConfig;

View 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 { GenericFtrProviderContext } from '@kbn/test';
export type FtrProviderContext = GenericFtrProviderContext<{}, {}>;

View file

@ -0,0 +1,6 @@
{
"type": "test-helper",
"id": "@kbn/inventory-e2e",
"owner": "@elastic/obs-ux-infra_services-team",
"devOnly": true
}

View file

@ -0,0 +1,118 @@
/*
* 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 {
ApmSynthtraceEsClient,
EntitiesSynthtraceEsClient,
LogLevel,
LogsSynthtraceEsClient,
createLogger,
} from '@kbn/apm-synthtrace';
import { createEsClientForTesting } from '@kbn/test';
// eslint-disable-next-line @kbn/imports/no_unresolvable_imports
import { initPlugin } from '@frsource/cypress-plugin-visual-regression-diff/plugins';
import del from 'del';
import { some } from 'lodash';
import { Readable } from 'stream';
export function setupNodeEvents(on: Cypress.PluginEvents, config: Cypress.PluginConfigOptions) {
const logger = createLogger(LogLevel.info);
const client = createEsClientForTesting({
esUrl: config.env.ES_NODE,
requestTimeout: config.env.ES_REQUEST_TIMEOUT,
isCloud: !!config.env.TEST_CLOUD,
});
const entitiesSynthtraceEsClient = new EntitiesSynthtraceEsClient({
client,
logger,
refreshAfterIndex: true,
});
const apmSynthtraceEsClient = new ApmSynthtraceEsClient({
client,
logger,
refreshAfterIndex: true,
version: config.env.APM_PACKAGE_VERSION,
});
const logsSynthtraceEsClient = new LogsSynthtraceEsClient({
client,
logger,
refreshAfterIndex: true,
});
entitiesSynthtraceEsClient.pipeline(
entitiesSynthtraceEsClient.getDefaultPipeline({ includeSerialization: false })
);
apmSynthtraceEsClient.pipeline(
apmSynthtraceEsClient.getDefaultPipeline({ includeSerialization: false })
);
logsSynthtraceEsClient.pipeline(
logsSynthtraceEsClient.getDefaultPipeline({ includeSerialization: false })
);
initPlugin(on, config);
on('task', {
// send logs to node process
log(message) {
// eslint-disable-next-line no-console
console.log(message);
return null;
},
async 'entitiesSynthtrace:index'(events: Array<Record<string, any>>) {
await entitiesSynthtraceEsClient.index(Readable.from(events));
return null;
},
async 'entitiesSynthtrace:clean'() {
await entitiesSynthtraceEsClient.clean();
return null;
},
async 'apmSynthtrace:index'(events: Array<Record<string, any>>) {
await apmSynthtraceEsClient.index(Readable.from(events));
return null;
},
async 'apmSynthtrace:clean'() {
await apmSynthtraceEsClient.clean();
return null;
},
async 'logsSynthtrace:index'(events: Array<Record<string, any>>) {
await logsSynthtraceEsClient.index(Readable.from(events));
return null;
},
async 'logsSynthtrace:clean'() {
await logsSynthtraceEsClient.clean();
return null;
},
});
on('after:spec', (spec, results) => {
// Delete videos that have no failures or retries
if (results && results.video) {
const failures = some(results.tests, (test) => {
return some(test.attempts, { state: 'failed' });
});
if (!failures) {
del(results.video);
}
}
});
on('before:browser:launch', (browser, launchOptions) => {
if (browser.name === 'electron' && browser.isHeadless) {
launchOptions.preferences.width = 1440;
launchOptions.preferences.height = 1600;
}
return launchOptions;
});
}

View file

@ -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
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type {
Serializable,
SynthtraceGenerator,
EntityFields,
ApmFields,
} from '@kbn/apm-synthtrace-client';
export const entitiesSynthtrace = {
index: (events: SynthtraceGenerator<EntityFields> | Array<Serializable<EntityFields>>) =>
cy.task(
'entitiesSynthtrace:index',
Array.from(events).flatMap((event) => event.serialize())
),
clean: () => cy.task('entitiesSynthtrace:clean'),
};
export const apmSynthtrace = {
index: (events: SynthtraceGenerator<ApmFields> | Array<Serializable<ApmFields>>) =>
cy.task(
'apmSynthtrace:index',
Array.from(events).flatMap((event) => event.serialize())
),
clean: () => cy.task('apmSynthtrace:clean'),
};
export const logsSynthtrace = {
index: (events: SynthtraceGenerator<ApmFields> | Array<Serializable<ApmFields>>) =>
cy.task(
'logsSynthtrace:index',
Array.from(events).flatMap((event) => event.serialize())
),
clean: () => cy.task('logsSynthtrace:clean'),
};

View file

@ -0,0 +1,19 @@
{
"extends": "../../../../../tsconfig.base.json",
"include": ["**/*"],
"exclude": ["tmp", "target/**/*"],
"compilerOptions": {
"outDir": "target/types",
"types": ["cypress", "node", "cypress-real-events"],
"isolatedModules": false
},
"kbn_references": [
"@kbn/test",
"@kbn/apm-synthtrace",
"@kbn/apm-synthtrace-client",
"@kbn/dev-utils",
"@kbn/cypress-config",
"@kbn/ftr-common-functional-services",
"@kbn/ftr-common-functional-ui-services"
]
}

View file

@ -21,6 +21,7 @@ interface Props {
const toComboBoxOption = (entityType: EntityType): EuiComboBoxOptionOption<EntityType> => ({
key: entityType,
label: getEntityTypeLabel(entityType),
'data-test-subj': `entityTypesFilter${entityType}Option`,
});
export function EntityTypesControls({ onChange }: Props) {
@ -44,6 +45,7 @@ export function EntityTypesControls({ onChange }: Props) {
return (
<EuiComboBox<EntityType>
data-test-subj="entityTypesFilterComboBox"
isLoading={loading}
css={css`
max-width: 325px;

View file

@ -0,0 +1,102 @@
/*
* 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 path = require('path');
const yargs = require('yargs');
const childProcess = require('child_process');
const { REPO_ROOT } = require('@kbn/repo-info');
const { argv } = yargs(process.argv.slice(2))
.parserConfiguration({ 'unknown-options-as-args': true })
.option('kibana-install-dir', {
default: '',
type: 'string',
description: 'Path to the Kibana install directory',
})
.option('server', {
default: false,
type: 'boolean',
description: 'Start Elasticsearch and Kibana',
})
.option('runner', {
default: false,
type: 'boolean',
description:
'Run all tests (an instance of Elasticsearch and kibana are needs to be available)',
})
.option('times', {
type: 'number',
description: 'Repeat the test n number of times',
})
.option('bail', {
default: false,
type: 'boolean',
description: 'stop tests after the first failure',
})
.help();
const e2eDir = path.join(__dirname, '../../e2e');
let ftrScript = 'functional_tests.js';
if (argv.server) {
ftrScript = 'functional_tests_server.js';
} else if (argv.runner) {
ftrScript = 'functional_test_runner.js';
}
const cypressCliArgs = yargs(argv._).parserConfiguration({
'boolean-negation': false,
}).argv;
if (cypressCliArgs.grep) {
throw new Error('--grep is not supported. Please use --spec instead');
}
const spawnArgs = [
`${REPO_ROOT}/scripts/${ftrScript}`,
`--config=./ftr_config.ts`,
`--kibana-install-dir=${argv.kibanaInstallDir}`,
...(argv.bail ? [`--bail`] : []),
];
function runTests() {
console.log(`Running e2e tests: "node ${spawnArgs.join(' ')}"`);
return childProcess.spawnSync('node', spawnArgs, {
cwd: e2eDir,
env: {
...process.env,
CYPRESS_CLI_ARGS: JSON.stringify(cypressCliArgs),
NODE_OPTIONS: '--openssl-legacy-provider',
},
encoding: 'utf8',
stdio: 'inherit',
});
}
const runCounter = { succeeded: 0, failed: 0, remaining: argv.times };
let exitStatus = 0;
times(argv.times ?? 1, () => {
const child = runTests();
if (child.status === 0) {
runCounter.succeeded++;
} else {
exitStatus = child.status;
runCounter.failed++;
}
runCounter.remaining--;
if (argv.times > 1) {
console.log(runCounter);
}
});
process.exitCode = exitStatus;
console.log(`Quitting with exit code ${exitStatus}`);

View file

@ -5419,6 +5419,10 @@
version "0.0.0"
uid ""
"@kbn/inventory-e2e@link:x-pack/plugins/observability_solution/inventory/e2e":
version "0.0.0"
uid ""
"@kbn/inventory-plugin@link:x-pack/plugins/observability_solution/inventory":
version "0.0.0"
uid ""