diff --git a/.buildkite/ftr_oblt_stateful_configs.yml b/.buildkite/ftr_oblt_stateful_configs.yml index 06c99e53e767a..1d6d834b74279 100644 --- a/.buildkite/ftr_oblt_stateful_configs.yml +++ b/.buildkite/ftr_oblt_stateful_configs.yml @@ -50,5 +50,5 @@ enabled: - x-pack/test/observability_ai_assistant_functional/enterprise/config.ts - x-pack/test/profiling_api_integration/cloud/config.ts - x-pack/test/functional/apps/apm/config.ts - # stateful config files that run deployment-agnostic tests + # stateful configs that run deployment-agnostic tests - x-pack/test/api_integration/deployment_agnostic/configs/stateful/oblt.stateful.config.ts diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 425fdc7303700..113c79a2dc0a0 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1142,7 +1142,9 @@ x-pack/test/observability_ai_assistant_functional @elastic/obs-ai-assistant /x-pack/test_serverless/**/test_suites/observability/custom_threshold_rule/ @elastic/obs-ux-management-team /x-pack/test_serverless/**/test_suites/observability/slos/ @elastic/obs-ux-management-team /x-pack/test_serverless/api_integration/test_suites/observability/es_query_rule @elastic/obs-ux-management-team -/x-pack/test_serverless/api_integration/test_suites/observability/burn_rate_rule @elastic/obs-ux-management-team +/x-pack/test/api_integration/deployment_agnostic/apis/observability/alerting/burn_rate_rule @elastic/obs-ux-management-team +/x-pack/test/api_integration/deployment_agnostic/services/alerting_api @elastic/obs-ux-management-team +/x-pack/test/api_integration/deployment_agnostic/services/slo_api @elastic/obs-ux-management-team /x-pack/test_serverless/**/test_suites/observability/infra/ @elastic/obs-ux-infra_services-team # Elastic Stack Monitoring diff --git a/x-pack/test_serverless/api_integration/test_suites/observability/burn_rate_rule/burn_rate_rule.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/alerting/burn_rate_rule.ts similarity index 82% rename from x-pack/test_serverless/api_integration/test_suites/observability/burn_rate_rule/burn_rate_rule.ts rename to x-pack/test/api_integration/deployment_agnostic/apis/observability/alerting/burn_rate_rule.ts index 5378110307487..22a70ad439851 100644 --- a/x-pack/test_serverless/api_integration/test_suites/observability/burn_rate_rule/burn_rate_rule.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/alerting/burn_rate_rule.ts @@ -4,46 +4,41 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.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 { cleanup, Dataset, generate, PartialConfig } from '@kbn/data-forge'; +import { RoleCredentials, InternalRequestHeader } from '@kbn/ftr-common-functional-services'; import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../ftr_provider_context'; -import { InternalRequestHeader, RoleCredentials } from '../../../../shared/services'; +import { DeploymentAgnosticFtrProviderContext } from '../../../ftr_provider_context'; -export default function ({ getService }: FtrProviderContext) { +export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { const esClient = getService('es'); - const supertest = getService('supertest'); const esDeleteAllIndices = getService('esDeleteAllIndices'); + const samlAuth = getService('samlAuth'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); const logger = getService('log'); const alertingApi = getService('alertingApi'); const dataViewApi = getService('dataViewApi'); const sloApi = getService('sloApi'); - const svlUserManager = getService('svlUserManager'); - const svlCommonApi = getService('svlCommonApi'); - let roleAuthc: RoleCredentials; - let internalReqHeader: InternalRequestHeader; + const config = getService('config'); + const isServerless = config.get('serverless'); + const expectedConsumer = isServerless ? 'observability' : 'slo'; describe('Burn rate rule', () => { const RULE_TYPE_ID = 'slo.rules.burnRate'; const DATA_VIEW = 'kbn-data-forge-fake_hosts.fake_hosts-*'; const RULE_ALERT_INDEX = '.alerts-observability.slo.alerts-default'; - const ALERT_ACTION_INDEX = 'alert-action-slo'; const DATA_VIEW_ID = 'data-view-id'; let dataForgeConfig: PartialConfig; let dataForgeIndices: string[]; let actionId: string; let ruleId: string; + let adminRoleAuthc: RoleCredentials; + let internalHeaders: InternalRequestHeader; before(async () => { - roleAuthc = await svlUserManager.createM2mApiKeyWithRoleScope('admin'); - internalReqHeader = svlCommonApi.getInternalRequestHeader(); + adminRoleAuthc = await samlAuth.createM2mApiKeyWithRoleScope('admin'); + internalHeaders = samlAuth.getInternalRequestHeader(); dataForgeConfig = { schedule: [ { @@ -60,37 +55,49 @@ export default function ({ getService }: FtrProviderContext) { indexing: { dataset: 'fake_hosts' as Dataset, eventsPerCycle: 1, interval: 10000 }, }; dataForgeIndices = await generate({ client: esClient, config: dataForgeConfig, logger }); - await alertingApi.waitForDocumentInIndex({ indexName: DATA_VIEW, docCountTarget: 360 }); + await alertingApi.waitForDocumentInIndex({ + indexName: DATA_VIEW, + docCountTarget: 360, + }); await dataViewApi.create({ + roleAuthc: adminRoleAuthc, name: DATA_VIEW, id: DATA_VIEW_ID, title: DATA_VIEW, }); - roleAuthc = await svlUserManager.createM2mApiKeyWithRoleScope('admin'); }); after(async () => { - await supertest.delete(`/api/alerting/rule/${ruleId}`).set(internalReqHeader); - await supertest.delete(`/api/actions/connector/${actionId}`).set(internalReqHeader); + await supertestWithoutAuth + .delete(`/api/alerting/rule/${ruleId}`) + .set(adminRoleAuthc.apiKeyHeader) + .set(internalHeaders); + await supertestWithoutAuth + .delete(`/api/actions/connector/${actionId}`) + .set(adminRoleAuthc.apiKeyHeader) + .set(internalHeaders); await esClient.deleteByQuery({ index: '.kibana-event-log-*', query: { term: { 'rule.id': ruleId } }, conflicts: 'proceed', }); await dataViewApi.delete({ + roleAuthc: adminRoleAuthc, id: DATA_VIEW_ID, }); - await supertest.delete('/api/observability/slos/my-custom-id').set(internalReqHeader); - + await supertestWithoutAuth + .delete('/api/observability/slos/my-custom-id') + .set(adminRoleAuthc.apiKeyHeader) + .set(internalHeaders); await esDeleteAllIndices([ALERT_ACTION_INDEX, ...dataForgeIndices]); await cleanup({ client: esClient, config: dataForgeConfig, logger }); - await svlUserManager.invalidateM2mApiKeyWithRoleScope(roleAuthc); + await samlAuth.invalidateM2mApiKeyWithRoleScope(adminRoleAuthc); }); describe('Rule creation', () => { it('creates rule successfully', async () => { actionId = await alertingApi.createIndexConnector({ - roleAuthc, + roleAuthc: adminRoleAuthc, name: 'Index Connector: Slo Burn rate API test', indexName: ALERT_ACTION_INDEX, }); @@ -119,13 +126,13 @@ export default function ({ getService }: FtrProviderContext) { }, groupBy: '*', }, - roleAuthc + adminRoleAuthc ); const dependencyRule = await alertingApi.createRule({ - roleAuthc, + roleAuthc: adminRoleAuthc, tags: ['observability'], - consumer: 'observability', + consumer: expectedConsumer, name: 'SLO Burn Rate rule - Dependency', ruleTypeId: RULE_TYPE_ID, schedule: { @@ -196,9 +203,9 @@ export default function ({ getService }: FtrProviderContext) { }); const createdRule = await alertingApi.createRule({ - roleAuthc, + roleAuthc: adminRoleAuthc, tags: ['observability'], - consumer: 'observability', + consumer: expectedConsumer, name: 'SLO Burn Rate rule', ruleTypeId: RULE_TYPE_ID, schedule: { @@ -279,7 +286,7 @@ export default function ({ getService }: FtrProviderContext) { it('should be active', async () => { const executionStatus = await alertingApi.waitForRuleStatus({ - roleAuthc, + roleAuthc: adminRoleAuthc, ruleId, expectedStatus: 'active', }); @@ -299,9 +306,9 @@ export default function ({ getService }: FtrProviderContext) { }); it('should find the created rule with correct information about the consumer', async () => { - const match = await alertingApi.findRule(roleAuthc, ruleId); + const match = await alertingApi.findRule(ruleId, adminRoleAuthc); expect(match).not.to.be(undefined); - expect(match.consumer).to.be('observability'); + expect(match.consumer).to.be(expectedConsumer); }); }); }); diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/alerting/index.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/alerting/index.ts new file mode 100644 index 0000000000000..b2dc2abeca67d --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/alerting/index.ts @@ -0,0 +1,14 @@ +/* + * 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 { DeploymentAgnosticFtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ loadTestFile }: DeploymentAgnosticFtrProviderContext) { + describe('Slo - Burn rate rule', () => { + loadTestFile(require.resolve('./burn_rate_rule')); + }); +} diff --git a/x-pack/test/api_integration/deployment_agnostic/configs/serverless/oblt.index.ts b/x-pack/test/api_integration/deployment_agnostic/configs/serverless/oblt.index.ts index 3acc5cba6067e..6a2a8c3a7a51e 100644 --- a/x-pack/test/api_integration/deployment_agnostic/configs/serverless/oblt.index.ts +++ b/x-pack/test/api_integration/deployment_agnostic/configs/serverless/oblt.index.ts @@ -12,5 +12,6 @@ export default function ({ loadTestFile }: DeploymentAgnosticFtrProviderContext) loadTestFile(require.resolve('../../apis/console')); loadTestFile(require.resolve('../../apis/core')); loadTestFile(require.resolve('../../apis/painless_lab')); + loadTestFile(require.resolve('../../apis/observability/alerting')); }); } diff --git a/x-pack/test/api_integration/deployment_agnostic/configs/stateful/oblt.index.ts b/x-pack/test/api_integration/deployment_agnostic/configs/stateful/oblt.index.ts index d003c3c625b33..b4b28a10a40d7 100644 --- a/x-pack/test/api_integration/deployment_agnostic/configs/stateful/oblt.index.ts +++ b/x-pack/test/api_integration/deployment_agnostic/configs/stateful/oblt.index.ts @@ -7,8 +7,9 @@ import { DeploymentAgnosticFtrProviderContext } from '../../ftr_provider_context'; -export default function ({}: DeploymentAgnosticFtrProviderContext) { +export default function ({ loadTestFile }: DeploymentAgnosticFtrProviderContext) { describe('apis', () => { // load new oblt deployment-agnostic test here + loadTestFile(require.resolve('../../apis/observability/alerting')); }); } diff --git a/x-pack/test/api_integration/deployment_agnostic/configs/stateful/oblt.stateful.config.ts b/x-pack/test/api_integration/deployment_agnostic/configs/stateful/oblt.stateful.config.ts index fdc0926fe9162..7b3cf3a7f1818 100644 --- a/x-pack/test/api_integration/deployment_agnostic/configs/stateful/oblt.stateful.config.ts +++ b/x-pack/test/api_integration/deployment_agnostic/configs/stateful/oblt.stateful.config.ts @@ -10,6 +10,6 @@ import { createStatefulTestConfig } from '../../default_configs/stateful.config. export default createStatefulTestConfig({ testFiles: [require.resolve('./oblt.index.ts')], junit: { - reportName: 'Observibility Stateful - Deployment-agnostic API Integration Tests', + reportName: 'Stateful Observability - Deployment-agnostic API Integration Tests', }, }); diff --git a/x-pack/test/api_integration/deployment_agnostic/services/alerting_api.ts b/x-pack/test/api_integration/deployment_agnostic/services/alerting_api.ts new file mode 100644 index 0000000000000..430fd51efdc32 --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/services/alerting_api.ts @@ -0,0 +1,203 @@ +/* + * 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 { + AggregationsAggregate, + SearchResponse, +} from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { MetricThresholdParams } from '@kbn/infra-plugin/common/alerting/metrics'; +import { ThresholdParams } from '@kbn/observability-plugin/common/custom_threshold_rule/types'; +import { RoleCredentials } from '@kbn/ftr-common-functional-services'; +import { DeploymentAgnosticFtrProviderContext } from '../ftr_provider_context'; + +export interface SloBurnRateRuleParams { + sloId: string; + windows: WindowSchema[]; + dependencies?: Dependency[]; +} + +interface WindowSchema { + id: string; + burnRateThreshold: number; + maxBurnRateThreshold: number; + longWindow: Duration; + shortWindow: Duration; + actionGroup: string; +} + +interface Dependency { + ruleId: string; + actionGroupsToSuppressOn: string[]; +} + +type DurationUnit = 'm' | 'h' | 'd' | 'w' | 'M'; + +interface Duration { + value: number; + unit: DurationUnit; +} + +export function AlertingApiProvider({ getService }: DeploymentAgnosticFtrProviderContext) { + const samlAuth = getService('samlAuth'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); + const es = getService('es'); + const retry = getService('retry'); + const logger = getService('log'); + const config = getService('config'); + const retryTimeout = config.get('timeouts.try'); + const requestTimeout = 30 * 1000; + + return { + async waitForRuleStatus({ + ruleId, + expectedStatus, + roleAuthc, + }: { + ruleId: string; + expectedStatus: string; + roleAuthc: RoleCredentials; + }) { + return await retry.tryForTime(retryTimeout, async () => { + const response = await supertestWithoutAuth + .get(`/api/alerting/rule/${ruleId}`) + .set(roleAuthc.apiKeyHeader) + .set(samlAuth.getInternalRequestHeader()) + .timeout(requestTimeout); + const { execution_status: executionStatus } = response.body || {}; + const { status } = executionStatus || {}; + if (status !== expectedStatus) { + throw new Error(`waitForStatus(${expectedStatus}): got ${status}`); + } + return executionStatus?.status; + }); + }, + + async waitForDocumentInIndex({ + indexName, + docCountTarget = 1, + }: { + indexName: string; + docCountTarget?: number; + }): Promise>> { + return await retry.tryForTime(retryTimeout, async () => { + const response = await es.search({ + index: indexName, + rest_total_hits_as_int: true, + }); + logger.debug(`Found ${response.hits.total} docs, looking for at least ${docCountTarget}.`); + + if ( + !response.hits.total || + (typeof response.hits.total === 'number' + ? response.hits.total < docCountTarget + : response.hits.total.value < docCountTarget) + ) { + throw new Error('No hits or not enough hits found'); + } + + return response; + }); + }, + + async waitForAlertInIndex({ + indexName, + ruleId, + }: { + indexName: string; + ruleId: string; + }): Promise>> { + if (!ruleId) { + throw new Error(`'ruleId' is undefined`); + } + return await retry.tryForTime(retryTimeout, async () => { + const response = await es.search({ + index: indexName, + body: { + query: { + term: { + 'kibana.alert.rule.uuid': ruleId, + }, + }, + }, + }); + if (response.hits.hits.length === 0) { + throw new Error('No hits found'); + } + return response; + }); + }, + + async createIndexConnector({ + name, + indexName, + roleAuthc, + }: { + name: string; + indexName: string; + roleAuthc: RoleCredentials; + }) { + const { body } = await supertestWithoutAuth + .post(`/api/actions/connector`) + .set(roleAuthc.apiKeyHeader) + .set(samlAuth.getInternalRequestHeader()) + .send({ + name, + config: { + index: indexName, + refresh: true, + }, + connector_type_id: '.index', + }); + return body.id as string; + }, + + async createRule({ + name, + ruleTypeId, + params, + actions = [], + tags = [], + schedule, + consumer, + roleAuthc, + }: { + ruleTypeId: string; + name: string; + params: MetricThresholdParams | ThresholdParams | SloBurnRateRuleParams; + actions?: any[]; + tags?: any[]; + schedule?: { interval: string }; + consumer: string; + roleAuthc: RoleCredentials; + }) { + const { body } = await supertestWithoutAuth + .post(`/api/alerting/rule`) + .set(roleAuthc.apiKeyHeader) + .set(samlAuth.getInternalRequestHeader()) + .send({ + params, + consumer, + schedule: schedule || { + interval: '5m', + }, + tags, + name, + rule_type_id: ruleTypeId, + actions, + }); + return body; + }, + + async findRule(ruleId: string, roleAuthc: RoleCredentials) { + const response = await supertestWithoutAuth + .get('/api/alerting/rules/_find') + .set(roleAuthc.apiKeyHeader) + .set(samlAuth.getInternalRequestHeader()); + return response.body.data.find((obj: any) => obj.id === ruleId); + }, + }; +} diff --git a/x-pack/test/api_integration/deployment_agnostic/services/deployment_agnostic_services.ts b/x-pack/test/api_integration/deployment_agnostic/services/deployment_agnostic_services.ts index 38222c096bed0..9623df1bebbd0 100644 --- a/x-pack/test/api_integration/deployment_agnostic/services/deployment_agnostic_services.ts +++ b/x-pack/test/api_integration/deployment_agnostic/services/deployment_agnostic_services.ts @@ -15,6 +15,7 @@ import { services as apiIntegrationServices } from '../../services'; export const deploymentAgnosticServices = _.pick(apiIntegrationServices, [ 'supertest', // TODO: review its behaviour 'es', + 'esDeleteAllIndices', // TODO: review its behaviour 'esArchiver', 'esSupertest', // TODO: review its behaviour 'indexPatterns', diff --git a/x-pack/test/api_integration/deployment_agnostic/services/index.ts b/x-pack/test/api_integration/deployment_agnostic/services/index.ts index 6988400e89fa3..88c2503c80c98 100644 --- a/x-pack/test/api_integration/deployment_agnostic/services/index.ts +++ b/x-pack/test/api_integration/deployment_agnostic/services/index.ts @@ -9,6 +9,7 @@ import { commonFunctionalServices } from '@kbn/ftr-common-functional-services'; import { deploymentAgnosticServices } from './deployment_agnostic_services'; import { DataViewApiProvider } from './data_view_api'; import { SloApiProvider } from './slo_api'; +import { AlertingApiProvider } from './alerting_api'; import { RoleScopedSupertestProvider, SupertestWithRoleScope } from './role_scoped_supertest'; export type { @@ -21,6 +22,7 @@ export const services = { ...deploymentAgnosticServices, supertestWithoutAuth: commonFunctionalServices.supertestWithoutAuth, samlAuth: commonFunctionalServices.samlAuth, + alertingApi: AlertingApiProvider, dataViewApi: DataViewApiProvider, sloApi: SloApiProvider, roleScopedSupertest: RoleScopedSupertestProvider, diff --git a/x-pack/test/api_integration/deployment_agnostic/services/slo_api.ts b/x-pack/test/api_integration/deployment_agnostic/services/slo_api.ts index 4c83a536ffb36..05db3259ddbc6 100644 --- a/x-pack/test/api_integration/deployment_agnostic/services/slo_api.ts +++ b/x-pack/test/api_integration/deployment_agnostic/services/slo_api.ts @@ -13,33 +13,6 @@ import * as t from 'io-ts'; import { RoleCredentials } from '@kbn/ftr-common-functional-services'; import { DeploymentAgnosticFtrProviderContext } from '../ftr_provider_context'; -type DurationUnit = 'm' | 'h' | 'd' | 'w' | 'M'; - -interface Duration { - value: number; - unit: DurationUnit; -} - -interface WindowSchema { - id: string; - burnRateThreshold: number; - maxBurnRateThreshold: number; - longWindow: Duration; - shortWindow: Duration; - actionGroup: string; -} - -interface Dependency { - ruleId: string; - actionGroupsToSuppressOn: string[]; -} - -export interface SloBurnRateRuleParams { - sloId: string; - windows: WindowSchema[]; - dependencies?: Dependency[]; -} - interface SloParams { id?: string; name: string; diff --git a/x-pack/test_serverless/api_integration/test_suites/observability/index.ts b/x-pack/test_serverless/api_integration/test_suites/observability/index.ts index c3e9bddc4e95c..a138c768d43d4 100644 --- a/x-pack/test_serverless/api_integration/test_suites/observability/index.ts +++ b/x-pack/test_serverless/api_integration/test_suites/observability/index.ts @@ -15,7 +15,6 @@ export default function ({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./apm_api_integration/service_maps/service_maps')); loadTestFile(require.resolve('./apm_api_integration/traces/critical_path')); loadTestFile(require.resolve('./cases')); - loadTestFile(require.resolve('./burn_rate_rule/burn_rate_rule')); loadTestFile(require.resolve('./es_query_rule/es_query_rule')); loadTestFile(require.resolve('./slos')); loadTestFile(require.resolve('./synthetics'));