From 08747f361aa39ba0e1412d48dc9856ef0806cdf2 Mon Sep 17 00:00:00 2001 From: Kevin Delemme Date: Fri, 18 Oct 2024 15:33:15 -0400 Subject: [PATCH] chore(slo): Migrate to test agnostic framework (#195927) --- .../apis/observability/alerting/index.ts | 2 +- .../apis/observability/slo/create_slo.ts | 242 ++++++++++++++++++ .../apis/observability/slo/delete_slo.ts | 120 +++++++++ .../apis/observability/slo/find_slo.ts | 101 ++++++++ .../apis/observability/slo/fixtures/slo.ts | 33 +++ .../apis/observability/slo/get_slo.ts | 72 ++++++ .../observability/slo/helpers/dataforge.ts | 24 ++ .../observability/slo/helpers/transform.ts | 59 +++++ .../apis/observability/slo/index.ts | 19 ++ .../apis/observability/slo/reset_slo.ts | 76 ++++++ .../apis/observability/slo/update_slo.ts | 93 +++++++ .../configs/serverless/oblt.index.ts | 1 + .../configs/stateful/oblt.index.ts | 1 + .../deployment_agnostic/services/slo_api.ts | 162 ++++-------- 14 files changed, 886 insertions(+), 119 deletions(-) create mode 100644 x-pack/test/api_integration/deployment_agnostic/apis/observability/slo/create_slo.ts create mode 100644 x-pack/test/api_integration/deployment_agnostic/apis/observability/slo/delete_slo.ts create mode 100644 x-pack/test/api_integration/deployment_agnostic/apis/observability/slo/find_slo.ts create mode 100644 x-pack/test/api_integration/deployment_agnostic/apis/observability/slo/fixtures/slo.ts create mode 100644 x-pack/test/api_integration/deployment_agnostic/apis/observability/slo/get_slo.ts create mode 100644 x-pack/test/api_integration/deployment_agnostic/apis/observability/slo/helpers/dataforge.ts create mode 100644 x-pack/test/api_integration/deployment_agnostic/apis/observability/slo/helpers/transform.ts create mode 100644 x-pack/test/api_integration/deployment_agnostic/apis/observability/slo/index.ts create mode 100644 x-pack/test/api_integration/deployment_agnostic/apis/observability/slo/reset_slo.ts create mode 100644 x-pack/test/api_integration/deployment_agnostic/apis/observability/slo/update_slo.ts 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 index b2dc2abeca67d..336fcf65c830f 100644 --- 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 @@ -8,7 +8,7 @@ import { DeploymentAgnosticFtrProviderContext } from '../../../ftr_provider_context'; export default function ({ loadTestFile }: DeploymentAgnosticFtrProviderContext) { - describe('Slo - Burn rate rule', () => { + describe('SLO - Burn rate rule', () => { loadTestFile(require.resolve('./burn_rate_rule')); }); } diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/slo/create_slo.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/slo/create_slo.ts new file mode 100644 index 0000000000000..28cef8c2c566c --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/slo/create_slo.ts @@ -0,0 +1,242 @@ +/* + * 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, generate } from '@kbn/data-forge'; +import expect from '@kbn/expect'; +import { RoleCredentials } from '@kbn/ftr-common-functional-services'; +import { getSLOSummaryTransformId, getSLOTransformId } from '@kbn/slo-plugin/common/constants'; +import { DeploymentAgnosticFtrProviderContext } from '../../../ftr_provider_context'; +import { DEFAULT_SLO } from './fixtures/slo'; +import { DATA_FORGE_CONFIG } from './helpers/dataforge'; +import { TransformHelper, createTransformHelper } from './helpers/transform'; + +export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { + const esClient = getService('es'); + const sloApi = getService('sloApi'); + const logger = getService('log'); + const retry = getService('retry'); + const samlAuth = getService('samlAuth'); + const dataViewApi = getService('dataViewApi'); + + const DATA_VIEW = 'kbn-data-forge-fake_hosts.fake_hosts-*'; + const DATA_VIEW_ID = 'data-view-id'; + + let adminRoleAuthc: RoleCredentials; + let transformHelper: TransformHelper; + + describe('Create SLOs', function () { + before(async () => { + adminRoleAuthc = await samlAuth.createM2mApiKeyWithRoleScope('admin'); + transformHelper = createTransformHelper(getService); + + await generate({ client: esClient, config: DATA_FORGE_CONFIG, logger }); + + await dataViewApi.create({ + roleAuthc: adminRoleAuthc, + name: DATA_VIEW, + id: DATA_VIEW_ID, + title: DATA_VIEW, + }); + + await sloApi.deleteAllSLOs(adminRoleAuthc); + }); + + after(async () => { + await dataViewApi.delete({ roleAuthc: adminRoleAuthc, id: DATA_VIEW_ID }); + await cleanup({ client: esClient, config: DATA_FORGE_CONFIG, logger }); + await sloApi.deleteAllSLOs(adminRoleAuthc); + await samlAuth.invalidateM2mApiKeyWithRoleScope(adminRoleAuthc); + }); + + it('creates a new slo and transforms', async () => { + const apiResponse = await sloApi.create(DEFAULT_SLO, adminRoleAuthc); + expect(apiResponse).property('id'); + const { id } = apiResponse; + + const definitions = await sloApi.findDefinitions(adminRoleAuthc); + expect(definitions.total).eql(1); + expect(definitions.results[0]).eql({ + budgetingMethod: 'occurrences', + updatedAt: definitions.results[0].updatedAt, + createdAt: definitions.results[0].createdAt, + description: 'Fixture for api integration tests', + enabled: true, + groupBy: 'tags', + id, + indicator: { + params: { + filter: 'system.network.name: eth1', + good: 'container.cpu.user.pct < 1', + index: 'kbn-data-forge*', + timestampField: '@timestamp', + total: 'container.cpu.user.pct: *', + }, + type: 'sli.kql.custom', + }, + name: 'Test SLO for api integration', + objective: { + target: 0.99, + }, + revision: 1, + settings: { + frequency: '1m', + syncDelay: '1m', + preventInitialBackfill: false, + }, + tags: ['test'], + timeWindow: { + duration: '7d', + type: 'rolling', + }, + version: 2, + }); + + const rollUpTransformResponse = await transformHelper.assertExist(getSLOTransformId(id, 1)); + expect(rollUpTransformResponse.transforms[0].source.index).eql(['kbn-data-forge*']); + expect(rollUpTransformResponse.transforms[0].dest).eql({ + index: '.slo-observability.sli-v3.3', + pipeline: `.slo-observability.sli.pipeline-${id}-1`, + }); + expect(rollUpTransformResponse.transforms[0].pivot.group_by).eql({ + 'slo.groupings.tags': { terms: { field: 'tags' } }, + '@timestamp': { date_histogram: { field: '@timestamp', fixed_interval: '1m' } }, + }); + + const summaryTransformResponse = await transformHelper.assertExist( + getSLOSummaryTransformId(id, 1) + ); + expect(summaryTransformResponse.transforms[0].source.index).eql([ + '.slo-observability.sli-v3.3*', + ]); + expect(summaryTransformResponse.transforms[0].dest).eql({ + index: '.slo-observability.summary-v3.3', + pipeline: `.slo-observability.summary.pipeline-${id}-1`, + }); + }); + + describe('groupBy smoke tests', () => { + it('creates instanceId for SLOs with multi groupBy', async () => { + const apiResponse = await sloApi.create( + Object.assign({}, DEFAULT_SLO, { groupBy: ['system.network.name', 'event.dataset'] }), + adminRoleAuthc + ); + + expect(apiResponse).property('id'); + const { id } = apiResponse; + + await retry.tryForTime(180 * 1000, async () => { + const response = await esClient.search(getRollupDataEsQuery(id)); + + // @ts-ignore + expect(response.aggregations?.last_doc.hits?.hits[0]._source.slo.instanceId).eql( + 'eth1,system.network' + ); + }); + }); + + it('creates instanceId for SLOs with single groupBy', async () => { + const apiResponse = await sloApi.create( + Object.assign({}, DEFAULT_SLO, { groupBy: 'system.network.name' }), + adminRoleAuthc + ); + + expect(apiResponse).property('id'); + const { id } = apiResponse; + + await retry.tryForTime(180 * 1000, async () => { + const response = await esClient.search(getRollupDataEsQuery(id)); + + // @ts-ignore + expect(response.aggregations?.last_doc.hits?.hits[0]._source.slo.instanceId).eql('eth1'); + }); + }); + + it('creates instanceId for SLOs without groupBy ([])', async () => { + const apiResponse = await sloApi.create( + Object.assign({}, DEFAULT_SLO, { groupBy: [] }), + adminRoleAuthc + ); + + expect(apiResponse).property('id'); + const { id } = apiResponse; + + await retry.tryForTime(300 * 1000, async () => { + const response = await esClient.search(getRollupDataEsQuery(id)); + + // @ts-ignore + expect(response.aggregations?.last_doc.hits?.hits[0]._source.slo.instanceId).eql('*'); + }); + }); + + it('creates instanceId for SLOs without groupBy (["*"])', async () => { + const apiResponse = await sloApi.create( + Object.assign({}, DEFAULT_SLO, { groupBy: ['*'] }), + adminRoleAuthc + ); + + expect(apiResponse).property('id'); + const { id } = apiResponse; + + await retry.tryForTime(180 * 1000, async () => { + const response = await esClient.search(getRollupDataEsQuery(id)); + + // @ts-ignore + expect(response.aggregations?.last_doc.hits?.hits[0]._source.slo.instanceId).eql('*'); + }); + }); + + it('creates instanceId for SLOs without groupBy ("")', async () => { + const apiResponse = await sloApi.create( + Object.assign({}, DEFAULT_SLO, { groupBy: '' }), + adminRoleAuthc + ); + expect(apiResponse).property('id'); + const { id } = apiResponse; + + await retry.tryForTime(180 * 1000, async () => { + const response = await esClient.search(getRollupDataEsQuery(id)); + + // @ts-ignore + expect(response.aggregations?.last_doc.hits?.hits[0]._source.slo.instanceId).eql('*'); + }); + }); + }); + }); +} + +const getRollupDataEsQuery = (id: string) => ({ + index: '.slo-observability.sli-v3*', + size: 0, + query: { + bool: { + filter: [ + { + term: { + 'slo.id': id, + }, + }, + ], + }, + }, + aggs: { + last_doc: { + top_hits: { + sort: [ + { + '@timestamp': { + order: 'desc', + }, + }, + ], + _source: { + includes: ['slo.instanceId'], + }, + size: 1, + }, + }, + }, +}); diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/slo/delete_slo.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/slo/delete_slo.ts new file mode 100644 index 0000000000000..733d2b6250c29 --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/slo/delete_slo.ts @@ -0,0 +1,120 @@ +/* + * 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, generate } from '@kbn/data-forge'; +import expect from '@kbn/expect'; +import { RoleCredentials } from '@kbn/ftr-common-functional-services'; +import { + SLO_DESTINATION_INDEX_PATTERN, + SLO_SUMMARY_DESTINATION_INDEX_PATTERN, + getSLOSummaryTransformId, + getSLOTransformId, +} from '@kbn/slo-plugin/common/constants'; +import { DeploymentAgnosticFtrProviderContext } from '../../../ftr_provider_context'; +import { DEFAULT_SLO } from './fixtures/slo'; +import { DATA_FORGE_CONFIG } from './helpers/dataforge'; +import { TransformHelper, createTransformHelper } from './helpers/transform'; + +export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { + const esClient = getService('es'); + const sloApi = getService('sloApi'); + const logger = getService('log'); + const retry = getService('retry'); + const samlAuth = getService('samlAuth'); + const dataViewApi = getService('dataViewApi'); + + const DATA_VIEW = 'kbn-data-forge-fake_hosts.fake_hosts-*'; + const DATA_VIEW_ID = 'data-view-id'; + + let adminRoleAuthc: RoleCredentials; + let transformHelper: TransformHelper; + + describe('Delete SLOs', function () { + before(async () => { + adminRoleAuthc = await samlAuth.createM2mApiKeyWithRoleScope('admin'); + transformHelper = createTransformHelper(getService); + + await generate({ client: esClient, config: DATA_FORGE_CONFIG, logger }); + + await dataViewApi.create({ + roleAuthc: adminRoleAuthc, + name: DATA_VIEW, + id: DATA_VIEW_ID, + title: DATA_VIEW, + }); + + await sloApi.deleteAllSLOs(adminRoleAuthc); + }); + + after(async () => { + await dataViewApi.delete({ roleAuthc: adminRoleAuthc, id: DATA_VIEW_ID }); + await cleanup({ client: esClient, config: DATA_FORGE_CONFIG, logger }); + await sloApi.deleteAllSLOs(adminRoleAuthc); + await samlAuth.invalidateM2mApiKeyWithRoleScope(adminRoleAuthc); + }); + + it('deletes SLO and related resources', async () => { + const response = await sloApi.create(DEFAULT_SLO, adminRoleAuthc); + expect(response).property('id'); + const id = response.id; + + await sloApi.delete(id, adminRoleAuthc); + + // Expect no definitions exists + const definitions = await sloApi.findDefinitions(adminRoleAuthc); + expect(definitions.total).eql(0); + + await transformHelper.assertNotFound(getSLOTransformId(id, 1)); + await transformHelper.assertNotFound(getSLOSummaryTransformId(id, 1)); + // expect summary and rollup documents to be deleted + await retry.waitForWithTimeout('SLO summary data is deleted', 60 * 1000, async () => { + const sloSummaryResponseAfterDeletion = await esClient.search({ + index: SLO_SUMMARY_DESTINATION_INDEX_PATTERN, + body: { + query: { + bool: { + filter: [ + { + term: { 'slo.id': id }, + }, + { + term: { isTempDoc: false }, + }, + ], + }, + }, + }, + }); + if (sloSummaryResponseAfterDeletion.hits.hits.length > 0) { + throw new Error('SLO summary data not deleted yet'); + } + return true; + }); + + await retry.waitForWithTimeout('SLO rollup data is deleted', 60 * 1000, async () => { + const sloRollupResponseAfterDeletion = await esClient.search({ + index: SLO_DESTINATION_INDEX_PATTERN, + body: { + query: { + bool: { + filter: [ + { + term: { 'slo.id': id }, + }, + ], + }, + }, + }, + }); + if (sloRollupResponseAfterDeletion.hits.hits.length > 1) { + throw new Error('SLO rollup data not deleted yet'); + } + return true; + }); + }); + }); +} diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/slo/find_slo.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/slo/find_slo.ts new file mode 100644 index 0000000000000..1d1be9dc338af --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/slo/find_slo.ts @@ -0,0 +1,101 @@ +/* + * 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, generate } from '@kbn/data-forge'; +import expect from '@kbn/expect'; +import { InternalRequestHeader, RoleCredentials } from '@kbn/ftr-common-functional-services'; +import { DeploymentAgnosticFtrProviderContext } from '../../../ftr_provider_context'; +import { DEFAULT_SLO } from './fixtures/slo'; +import { DATA_FORGE_CONFIG } from './helpers/dataforge'; + +export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { + const esClient = getService('es'); + const sloApi = getService('sloApi'); + const logger = getService('log'); + const retry = getService('retry'); + const samlAuth = getService('samlAuth'); + const dataViewApi = getService('dataViewApi'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); + + const DATA_VIEW = 'kbn-data-forge-fake_hosts.fake_hosts-*'; + const DATA_VIEW_ID = 'data-view-id'; + + let adminRoleAuthc: RoleCredentials; + let internalHeaders: InternalRequestHeader; + + describe('Find SLOs', function () { + before(async () => { + adminRoleAuthc = await samlAuth.createM2mApiKeyWithRoleScope('admin'); + internalHeaders = samlAuth.getInternalRequestHeader(); + + await generate({ client: esClient, config: DATA_FORGE_CONFIG, logger }); + + await dataViewApi.create({ + roleAuthc: adminRoleAuthc, + name: DATA_VIEW, + id: DATA_VIEW_ID, + title: DATA_VIEW, + }); + + await sloApi.deleteAllSLOs(adminRoleAuthc); + }); + + after(async () => { + await dataViewApi.delete({ roleAuthc: adminRoleAuthc, id: DATA_VIEW_ID }); + await cleanup({ client: esClient, config: DATA_FORGE_CONFIG, logger }); + await sloApi.deleteAllSLOs(adminRoleAuthc); + await samlAuth.invalidateM2mApiKeyWithRoleScope(adminRoleAuthc); + }); + + it('searches SLOs', async () => { + const createResponse1 = await sloApi.create(DEFAULT_SLO, adminRoleAuthc); + const createResponse2 = await sloApi.create( + Object.assign({}, DEFAULT_SLO, { name: 'something irrelevant foo' }), + adminRoleAuthc + ); + + const sloId1 = createResponse1.id; + const sloId2 = createResponse2.id; + + // search SLOs + await retry.tryForTime(180 * 1000, async () => { + let response = await supertestWithoutAuth + .get(`/api/observability/slos`) + .set(adminRoleAuthc.apiKeyHeader) + .set(internalHeaders) + .set('elastic-api-version', '1') + .send(); + + expect(response.body.results).length(2); + + response = await supertestWithoutAuth + .get(`/api/observability/slos?kqlQuery=slo.name%3Airrelevant`) + .set(adminRoleAuthc.apiKeyHeader) + .set(internalHeaders) + .set('elastic-api-version', '1') + .send() + .expect(200); + + expect(response.body.results).length(1); + expect(response.body.results[0].id).eql(sloId2); + + response = await supertestWithoutAuth + .get(`/api/observability/slos?kqlQuery=slo.name%3Aintegration`) + .set(adminRoleAuthc.apiKeyHeader) + .set(internalHeaders) + .set('elastic-api-version', '1') + .send() + .expect(200); + + expect(response.body.results).length(1); + expect(response.body.results[0].id).eql(sloId1); + + return true; + }); + }); + }); +} diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/slo/fixtures/slo.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/slo/fixtures/slo.ts new file mode 100644 index 0000000000000..dfc216760644c --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/slo/fixtures/slo.ts @@ -0,0 +1,33 @@ +/* + * 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 { CreateSLOInput } from '@kbn/slo-schema'; + +export const DEFAULT_SLO: CreateSLOInput = { + name: 'Test SLO for api integration', + description: 'Fixture for api integration tests', + indicator: { + type: 'sli.kql.custom', + params: { + index: 'kbn-data-forge*', + filter: 'system.network.name: eth1', + good: 'container.cpu.user.pct < 1', + total: 'container.cpu.user.pct: *', + timestampField: '@timestamp', + }, + }, + budgetingMethod: 'occurrences', + timeWindow: { + duration: '7d', + type: 'rolling', + }, + objective: { + target: 0.99, + }, + tags: ['test'], + groupBy: 'tags', +}; diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/slo/get_slo.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/slo/get_slo.ts new file mode 100644 index 0000000000000..7a27c3b36fb0d --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/slo/get_slo.ts @@ -0,0 +1,72 @@ +/* + * 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, generate } from '@kbn/data-forge'; +import expect from '@kbn/expect'; +import { RoleCredentials } from '@kbn/ftr-common-functional-services'; +import { DeploymentAgnosticFtrProviderContext } from '../../../ftr_provider_context'; +import { DEFAULT_SLO } from './fixtures/slo'; +import { DATA_FORGE_CONFIG } from './helpers/dataforge'; + +export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { + const esClient = getService('es'); + const sloApi = getService('sloApi'); + const logger = getService('log'); + const samlAuth = getService('samlAuth'); + const dataViewApi = getService('dataViewApi'); + + const DATA_VIEW = 'kbn-data-forge-fake_hosts.fake_hosts-*'; + const DATA_VIEW_ID = 'data-view-id'; + + let adminRoleAuthc: RoleCredentials; + + describe('Get SLOs', function () { + before(async () => { + adminRoleAuthc = await samlAuth.createM2mApiKeyWithRoleScope('admin'); + + await generate({ client: esClient, config: DATA_FORGE_CONFIG, logger }); + + await dataViewApi.create({ + roleAuthc: adminRoleAuthc, + name: DATA_VIEW, + id: DATA_VIEW_ID, + title: DATA_VIEW, + }); + + await sloApi.deleteAllSLOs(adminRoleAuthc); + }); + + after(async () => { + await dataViewApi.delete({ roleAuthc: adminRoleAuthc, id: DATA_VIEW_ID }); + await cleanup({ client: esClient, config: DATA_FORGE_CONFIG, logger }); + await sloApi.deleteAllSLOs(adminRoleAuthc); + await samlAuth.invalidateM2mApiKeyWithRoleScope(adminRoleAuthc); + }); + + it('get SLO by id', async () => { + const createResponse1 = await sloApi.create(DEFAULT_SLO, adminRoleAuthc); + await sloApi.create( + Object.assign({}, DEFAULT_SLO, { name: 'something irrelevant foo' }), + adminRoleAuthc + ); + + expect(createResponse1).property('id'); + const sloId1 = createResponse1.id; + + // get the slo by ID + const getSloResponse = await sloApi.get(sloId1, adminRoleAuthc); + // We cannot assert on the summary values itself - it would make the test too flaky + // But we can assert on the existence of these fields at least. + // On top of whatever the SLO definition contains. + expect(getSloResponse).property('summary'); + expect(getSloResponse).property('meta'); + expect(getSloResponse).property('instanceId'); + expect(getSloResponse.budgetingMethod).eql('occurrences'); + expect(getSloResponse.timeWindow).eql({ duration: '7d', type: 'rolling' }); + }); + }); +} diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/slo/helpers/dataforge.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/slo/helpers/dataforge.ts new file mode 100644 index 0000000000000..04da0f81a0643 --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/slo/helpers/dataforge.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { Dataset, PartialConfig } from '@kbn/data-forge/src/types'; + +export const DATA_FORGE_CONFIG: PartialConfig = { + schedule: [ + { + template: 'good', + start: 'now-15m', + end: 'now+5m', + metrics: [ + { name: 'system.cpu.user.pct', method: 'linear', start: 2.5, end: 2.5 }, + { name: 'system.cpu.total.pct', method: 'linear', start: 0.5, end: 0.5 }, + { name: 'system.cpu.total.norm.pct', method: 'linear', start: 0.8, end: 0.8 }, + ], + }, + ], + indexing: { dataset: 'fake_hosts' as Dataset, eventsPerCycle: 1 }, +}; diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/slo/helpers/transform.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/slo/helpers/transform.ts new file mode 100644 index 0000000000000..37b6ff1396c56 --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/slo/helpers/transform.ts @@ -0,0 +1,59 @@ +/* + * 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 type TransformHelper = ReturnType; + +export function createTransformHelper( + getService: DeploymentAgnosticFtrProviderContext['getService'] +) { + const retry = getService('retry'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); + const samlAuth = getService('samlAuth'); + + return { + assertNotFound: async (transformId: string) => { + const cookieHeader = await samlAuth.getM2MApiCookieCredentialsWithRoleScope('admin'); + + return await retry.tryWithRetries( + `Wait for transform ${transformId} to be deleted`, + async () => { + await supertestWithoutAuth + .get(`/internal/transform/transforms/${transformId}`) + .set(cookieHeader) + .set(samlAuth.getInternalRequestHeader()) + .set('elastic-api-version', '1') + .send() + .timeout(10000) + .expect(404); + }, + { retryCount: 10, retryDelay: 3000 } + ); + }, + + assertExist: async (transformId: string) => { + return await retry.tryWithRetries( + `Wait for transform ${transformId} to exist`, + async () => { + const cookieHeader = await samlAuth.getM2MApiCookieCredentialsWithRoleScope('admin'); + + const response = await supertestWithoutAuth + .get(`/internal/transform/transforms/${transformId}`) + .set(cookieHeader) + .set(samlAuth.getInternalRequestHeader()) + .set('elastic-api-version', '1') + .send() + .timeout(10000) + .expect(200); + return response.body; + }, + { retryCount: 10, retryDelay: 3000 } + ); + }, + }; +} diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/slo/index.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/slo/index.ts new file mode 100644 index 0000000000000..d47438d163b13 --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/slo/index.ts @@ -0,0 +1,19 @@ +/* + * 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', () => { + loadTestFile(require.resolve('./create_slo')); + loadTestFile(require.resolve('./delete_slo')); + loadTestFile(require.resolve('./get_slo')); + loadTestFile(require.resolve('./find_slo')); + loadTestFile(require.resolve('./reset_slo')); + loadTestFile(require.resolve('./update_slo')); + }); +} diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/slo/reset_slo.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/slo/reset_slo.ts new file mode 100644 index 0000000000000..c765c4ea55332 --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/slo/reset_slo.ts @@ -0,0 +1,76 @@ +/* + * 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, generate } from '@kbn/data-forge'; +import expect from '@kbn/expect'; +import { RoleCredentials } from '@kbn/ftr-common-functional-services'; +import { SLO_MODEL_VERSION, getSLOPipelineId } from '@kbn/slo-plugin/common/constants'; +import { DeploymentAgnosticFtrProviderContext } from '../../../ftr_provider_context'; +import { DEFAULT_SLO } from './fixtures/slo'; +import { DATA_FORGE_CONFIG } from './helpers/dataforge'; + +export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { + const esClient = getService('es'); + const sloApi = getService('sloApi'); + const logger = getService('log'); + const retry = getService('retry'); + const samlAuth = getService('samlAuth'); + const dataViewApi = getService('dataViewApi'); + + const DATA_VIEW = 'kbn-data-forge-fake_hosts.fake_hosts-*'; + const DATA_VIEW_ID = 'data-view-id'; + + let adminRoleAuthc: RoleCredentials; + + describe('Reset SLOs', function () { + before(async () => { + adminRoleAuthc = await samlAuth.createM2mApiKeyWithRoleScope('admin'); + + await generate({ client: esClient, config: DATA_FORGE_CONFIG, logger }); + + await dataViewApi.create({ + roleAuthc: adminRoleAuthc, + name: DATA_VIEW, + id: DATA_VIEW_ID, + title: DATA_VIEW, + }); + + await sloApi.deleteAllSLOs(adminRoleAuthc); + }); + + after(async () => { + await dataViewApi.delete({ roleAuthc: adminRoleAuthc, id: DATA_VIEW_ID }); + await cleanup({ client: esClient, config: DATA_FORGE_CONFIG, logger }); + await sloApi.deleteAllSLOs(adminRoleAuthc); + await samlAuth.invalidateM2mApiKeyWithRoleScope(adminRoleAuthc); + }); + + it('resets the related resources', async () => { + const createResponse = await sloApi.create(DEFAULT_SLO, adminRoleAuthc); + expect(createResponse).property('id'); + const sloId = createResponse.id; + const sloPipelineId = getSLOPipelineId(sloId, 1); + + // Delete the slo rollup ingest pipeline + await retry.tryForTime(60 * 1000, async () => { + await esClient.ingest.deletePipeline({ id: sloPipelineId }); + return true; + }); + + // reset + const resetResponse = await sloApi.reset(sloId, adminRoleAuthc); + expect(resetResponse).property('version', SLO_MODEL_VERSION); + expect(resetResponse).property('revision', 1); + + // assert the pipeline is re-created + await retry.tryForTime(60 * 1000, async () => { + const response = await esClient.ingest.getPipeline({ id: sloPipelineId }); + return !!response[sloPipelineId]; + }); + }); + }); +} diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/slo/update_slo.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/slo/update_slo.ts new file mode 100644 index 0000000000000..8946f2d613a99 --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/slo/update_slo.ts @@ -0,0 +1,93 @@ +/* + * 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, generate } from '@kbn/data-forge'; +import expect from '@kbn/expect'; +import { RoleCredentials } from '@kbn/ftr-common-functional-services'; +import { getSLOSummaryTransformId, getSLOTransformId } from '@kbn/slo-plugin/common/constants'; +import { DeploymentAgnosticFtrProviderContext } from '../../../ftr_provider_context'; +import { DEFAULT_SLO } from './fixtures/slo'; +import { DATA_FORGE_CONFIG } from './helpers/dataforge'; +import { TransformHelper, createTransformHelper } from './helpers/transform'; + +export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { + const esClient = getService('es'); + const sloApi = getService('sloApi'); + const logger = getService('log'); + const samlAuth = getService('samlAuth'); + const dataViewApi = getService('dataViewApi'); + + const DATA_VIEW = 'kbn-data-forge-fake_hosts.fake_hosts-*'; + const DATA_VIEW_ID = 'data-view-id'; + + let adminRoleAuthc: RoleCredentials; + let transformHelper: TransformHelper; + + describe('Update SLOs', function () { + before(async () => { + adminRoleAuthc = await samlAuth.createM2mApiKeyWithRoleScope('admin'); + transformHelper = createTransformHelper(getService); + + await generate({ client: esClient, config: DATA_FORGE_CONFIG, logger }); + + await dataViewApi.create({ + roleAuthc: adminRoleAuthc, + name: DATA_VIEW, + id: DATA_VIEW_ID, + title: DATA_VIEW, + }); + + await sloApi.deleteAllSLOs(adminRoleAuthc); + }); + + after(async () => { + await dataViewApi.delete({ roleAuthc: adminRoleAuthc, id: DATA_VIEW_ID }); + await cleanup({ client: esClient, config: DATA_FORGE_CONFIG, logger }); + await sloApi.deleteAllSLOs(adminRoleAuthc); + await samlAuth.invalidateM2mApiKeyWithRoleScope(adminRoleAuthc); + }); + + it('updates the definition without a revision bump', async () => { + const createResponse = await sloApi.create(DEFAULT_SLO, adminRoleAuthc); + const sloId = createResponse.id; + + const getResponse = await sloApi.get(sloId, adminRoleAuthc); + expect(getResponse).property('revision', 1); + + const updateResponse = await sloApi.update( + { sloId, slo: Object.assign({}, DEFAULT_SLO, { name: 'updated name' }) }, + adminRoleAuthc + ); + expect(updateResponse).property('revision', 1); + expect(updateResponse).property('name', 'updated name'); + + await transformHelper.assertExist(getSLOTransformId(sloId, 1)); + await transformHelper.assertExist(getSLOSummaryTransformId(sloId, 1)); + }); + + it('updates the definition with a revision bump', async () => { + const createResponse = await sloApi.create(DEFAULT_SLO, adminRoleAuthc); + const sloId = createResponse.id; + + const getResponse = await sloApi.get(sloId, adminRoleAuthc); + expect(getResponse).property('revision', 1); + + const updateResponse = await sloApi.update( + { sloId, slo: Object.assign({}, DEFAULT_SLO, { objective: { target: 0.63 } }) }, + adminRoleAuthc + ); + expect(updateResponse).property('revision', 2); + expect(updateResponse.objective).eql({ target: 0.63 }); + + await transformHelper.assertNotFound(getSLOTransformId(sloId, 1)); + await transformHelper.assertNotFound(getSLOSummaryTransformId(sloId, 1)); + + await transformHelper.assertExist(getSLOTransformId(sloId, 2)); + await transformHelper.assertExist(getSLOSummaryTransformId(sloId, 2)); + }); + }); +} 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 b11ced857253f..d9ee2daa42aac 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 @@ -18,5 +18,6 @@ export default function ({ loadTestFile }: DeploymentAgnosticFtrProviderContext) loadTestFile(require.resolve('../../apis/observability/dataset_quality')); loadTestFile(require.resolve('../../apis/painless_lab')); loadTestFile(require.resolve('../../apis/saved_objects_management')); + loadTestFile(require.resolve('../../apis/observability/slo')); }); } 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 cb51d672ab972..a467264698e57 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 @@ -12,5 +12,6 @@ export default function ({ loadTestFile }: DeploymentAgnosticFtrProviderContext) // load new oblt deployment-agnostic test here loadTestFile(require.resolve('../../apis/observability/alerting')); loadTestFile(require.resolve('../../apis/observability/dataset_quality')); + loadTestFile(require.resolve('../../apis/observability/slo')); }); } 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 05db3259ddbc6..8ee202b2cf23e 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 @@ -5,155 +5,81 @@ * 2.0. */ -import { - fetchHistoricalSummaryParamsSchema, - FetchHistoricalSummaryResponse, -} from '@kbn/slo-schema'; -import * as t from 'io-ts'; import { RoleCredentials } from '@kbn/ftr-common-functional-services'; +import { CreateSLOInput, FindSLODefinitionsResponse, UpdateSLOInput } from '@kbn/slo-schema'; import { DeploymentAgnosticFtrProviderContext } from '../ftr_provider_context'; -interface SloParams { - id?: string; - name: string; - description: string; - indicator: { - type: 'sli.kql.custom'; - params: { - index: string; - good: string; - total: string; - timestampField: string; - }; - }; - timeWindow: { - duration: string; - type: string; - }; - budgetingMethod: string; - objective: { - target: number; - }; - groupBy: string; -} - -type FetchHistoricalSummaryParams = t.OutputOf< - typeof fetchHistoricalSummaryParamsSchema.props.body ->; - -interface SloRequestParams { - id: string; - roleAuthc: RoleCredentials; -} - export function SloApiProvider({ getService }: DeploymentAgnosticFtrProviderContext) { - const es = getService('es'); const supertestWithoutAuth = getService('supertestWithoutAuth'); const samlAuth = getService('samlAuth'); - const retry = getService('retry'); - const config = getService('config'); - const retryTimeout = config.get('timeouts.try'); - const requestTimeout = 30 * 1000; return { - async create(slo: SloParams, roleAuthc: RoleCredentials) { + async create(slo: CreateSLOInput, roleAuthc: RoleCredentials) { const { body } = await supertestWithoutAuth .post(`/api/observability/slos`) .set(roleAuthc.apiKeyHeader) .set(samlAuth.getInternalRequestHeader()) - .send(slo); + .send(slo) + .expect(200); return body; }, - async delete({ id, roleAuthc }: SloRequestParams) { - const response = await supertestWithoutAuth - .delete(`/api/observability/slos/${id}`) + async reset(id: string, roleAuthc: RoleCredentials) { + const { body } = await supertestWithoutAuth + .post(`/api/observability/slos/${id}/_reset`) .set(roleAuthc.apiKeyHeader) - .set(samlAuth.getInternalRequestHeader()); - return response; + .set(samlAuth.getInternalRequestHeader()) + .send() + .expect(200); + + return body; }, - async fetchHistoricalSummary( - params: FetchHistoricalSummaryParams, + async update( + { sloId, slo }: { sloId: string; slo: UpdateSLOInput }, roleAuthc: RoleCredentials - ): Promise { + ) { const { body } = await supertestWithoutAuth - .post(`/internal/observability/slos/_historical_summary`) + .put(`/api/observability/slos/${sloId}`) .set(roleAuthc.apiKeyHeader) .set(samlAuth.getInternalRequestHeader()) - .send(params); + .send(slo) + .expect(200); + return body; }, - async waitForSloToBeDeleted({ id, roleAuthc }: SloRequestParams) { - return await retry.tryForTime(retryTimeout, async () => { - const response = await supertestWithoutAuth - .delete(`/api/observability/slos/${id}`) - .set(roleAuthc.apiKeyHeader) - .set(samlAuth.getInternalRequestHeader()) - .timeout(requestTimeout); - if (!response.ok) { - throw new Error(`SLO with id '${id}' was not deleted`); - } - return response; - }); + async delete(id: string, roleAuthc: RoleCredentials) { + return await supertestWithoutAuth + .delete(`/api/observability/slos/${id}`) + .set(roleAuthc.apiKeyHeader) + .set(samlAuth.getInternalRequestHeader()) + .send() + .expect(204); }, - async waitForSloCreated({ id, roleAuthc }: SloRequestParams) { - return await retry.tryForTime(retryTimeout, async () => { - const response = await supertestWithoutAuth - .get(`/api/observability/slos/${id}`) - .set(roleAuthc.apiKeyHeader) - .set(samlAuth.getInternalRequestHeader()) - .timeout(requestTimeout); - if (response.body.id === undefined) { - throw new Error(`No SLO with id '${id}' found`); - } - return response.body; - }); - }, + async get(id: string, roleAuthc: RoleCredentials) { + const { body } = await supertestWithoutAuth + .get(`/api/observability/slos/${id}`) + .set(roleAuthc.apiKeyHeader) + .set(samlAuth.getInternalRequestHeader()) + .send() + .expect(200); - async waitForSloSummaryTempIndexToExist(index: string) { - return await retry.tryForTime(retryTimeout, async () => { - const indexExists = await es.indices.exists({ index, allow_no_indices: false }); - if (!indexExists) { - throw new Error(`SLO summary index '${index}' should exist`); - } - return indexExists; - }); + return body; }, - async getSloData({ sloId, indexName }: { sloId: string; indexName: string }) { - const response = await es.search({ - index: indexName, - body: { - query: { - bool: { - filter: [{ term: { 'slo.id': sloId } }], - }, - }, - }, - }); - return response; - }, - async waitForSloData({ id, indexName }: { id: string; indexName: string }) { - return await retry.tryForTime(retryTimeout, async () => { - const response = await es.search({ - index: indexName, - body: { - query: { - bool: { - filter: [{ term: { 'slo.id': id } }], - }, - }, - }, - }); - if (response.hits.hits.length === 0) { - throw new Error(`No hits found at index '${indexName}' for slo id='${id}'`); - } - return response; - }); + async findDefinitions(roleAuthc: RoleCredentials): Promise { + const { body } = await supertestWithoutAuth + .get(`/api/observability/slos/_definitions`) + .set(roleAuthc.apiKeyHeader) + .set(samlAuth.getInternalRequestHeader()) + .send() + .expect(200); + + return body; }, + async deleteAllSLOs(roleAuthc: RoleCredentials) { const response = await supertestWithoutAuth .get(`/api/observability/slos/_definitions`)