From 96bfb638ed6aa2e1f06bba33162e6e8ac5fc0728 Mon Sep 17 00:00:00 2001 From: Dominique Clarke Date: Tue, 16 Jan 2024 10:37:45 -0500 Subject: [PATCH] [SLO] api integration tests stateful (#173236) ## Summary Adds tests for basic SLO api routes, including: 1. Find slos 2. Get slo by id 3. Get slo definitions 4. Get slo instances 5. Create slo 6. Delete slo 7. Update slo 8. Reset slo The create slo tests include some basic assertions that the resulting calculated SLO is correct. These tests do not cover: 1. SLOs in spaces 2. SLO permissions model Passed flaky test runner for 200 iterations: https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/4595#_ --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: shahzad31 Co-authored-by: Kevin Delemme --- .buildkite/ftr_configs.yml | 3 +- .../src/data_sources/fake_hosts/index.ts | 16 +- .../test/api_integration/apis/slos/config.ts | 29 + .../api_integration/apis/slos/create_slo.ts | 273 +++++++ .../api_integration/apis/slos/delete_slo.ts | 130 ++++ .../apis/slos/fixtures/create_slo.ts | 33 + .../test/api_integration/apis/slos/get_slo.ts | 465 ++++++++++++ .../api_integration/apis/slos/helper/es.ts | 64 ++ .../apis/slos/helper/load_test_data.ts | 23 + .../test/api_integration/apis/slos/index.ts | 18 + .../api_integration/apis/slos/reset_slo.ts | 105 +++ .../api_integration/apis/slos/update_slo.ts | 677 ++++++++++++++++++ .../api_integration/services/data_view_api.ts | 52 ++ x-pack/test/api_integration/services/index.ts | 4 + x-pack/test/api_integration/services/slo.ts | 51 ++ x-pack/test/tsconfig.json | 1 + 16 files changed, 1942 insertions(+), 2 deletions(-) create mode 100644 x-pack/test/api_integration/apis/slos/config.ts create mode 100644 x-pack/test/api_integration/apis/slos/create_slo.ts create mode 100644 x-pack/test/api_integration/apis/slos/delete_slo.ts create mode 100644 x-pack/test/api_integration/apis/slos/fixtures/create_slo.ts create mode 100644 x-pack/test/api_integration/apis/slos/get_slo.ts create mode 100644 x-pack/test/api_integration/apis/slos/helper/es.ts create mode 100644 x-pack/test/api_integration/apis/slos/helper/load_test_data.ts create mode 100644 x-pack/test/api_integration/apis/slos/index.ts create mode 100644 x-pack/test/api_integration/apis/slos/reset_slo.ts create mode 100644 x-pack/test/api_integration/apis/slos/update_slo.ts create mode 100644 x-pack/test/api_integration/services/data_view_api.ts create mode 100644 x-pack/test/api_integration/services/slo.ts diff --git a/.buildkite/ftr_configs.yml b/.buildkite/ftr_configs.yml index 6aefc30411141..ac929afea575b 100644 --- a/.buildkite/ftr_configs.yml +++ b/.buildkite/ftr_configs.yml @@ -214,6 +214,7 @@ enabled: - x-pack/test/api_integration/apis/stats/config.ts - x-pack/test/api_integration/apis/status/config.ts - x-pack/test/api_integration/apis/synthetics/config.ts + - x-pack/test/api_integration/apis/slos/config.ts - x-pack/test/api_integration/apis/telemetry/config.ts - x-pack/test/api_integration/apis/transform/config.ts - x-pack/test/api_integration/apis/upgrade_assistant/config.ts @@ -515,4 +516,4 @@ enabled: - x-pack/test/security_solution_api_integration/test_suites/detections_response/default_license/rule_read/configs/serverless.config.ts - x-pack/test/security_solution_api_integration/test_suites/detections_response/default_license/rule_read/configs/ess.config.ts - x-pack/test/security_solution_api_integration/test_suites/detections_response/default_license/rule_bulk_actions/configs/ess.config.ts - - x-pack/test/security_solution_api_integration/test_suites/detections_response/default_license/rule_bulk_actions/configs/serverless.config.ts + - x-pack/test/security_solution_api_integration/test_suites/detections_response/default_license/rule_bulk_actions/configs/serverless.config.ts \ No newline at end of file diff --git a/x-pack/packages/kbn-infra-forge/src/data_sources/fake_hosts/index.ts b/x-pack/packages/kbn-infra-forge/src/data_sources/fake_hosts/index.ts index e134cc21a0796..7e0e4546033e2 100644 --- a/x-pack/packages/kbn-infra-forge/src/data_sources/fake_hosts/index.ts +++ b/x-pack/packages/kbn-infra-forge/src/data_sources/fake_hosts/index.ts @@ -21,6 +21,19 @@ const generateNetworkData = lodash.memoize(() => { return networkDataCount; }); +let currentStatic = 0; + +const staticBetween = (end = 1, step = 0.1) => { + { + if (currentStatic + step > end) { + currentStatic = 0; + } else { + currentStatic = currentStatic + step; + } + return currentStatic; + } +}; + export const generateEvent = (index: number, timestamp: Moment, interval: number) => { const groupIndex = createGroupIndex(index); return [ @@ -142,13 +155,14 @@ export const generateEvent = (index: number, timestamp: Moment, interval: number }, }, user: { - pct: randomBetween(1, 4), + pct: staticBetween(1, 1), }, system: { pct: randomBetween(1, 4), }, }, }, + tags: [`${randomBetween(1, 4, 1)}`], }, ]; }; diff --git a/x-pack/test/api_integration/apis/slos/config.ts b/x-pack/test/api_integration/apis/slos/config.ts new file mode 100644 index 0000000000000..c755e2a46882d --- /dev/null +++ b/x-pack/test/api_integration/apis/slos/config.ts @@ -0,0 +1,29 @@ +/* + * 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'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const baseIntegrationTestsConfig = await readConfigFile(require.resolve('../../config.ts')); + + return { + ...baseIntegrationTestsConfig.getAll(), + testFiles: [require.resolve('.')], + // overriding default timeouts from packages/kbn-test/src/functional_test_runner/lib/config/schema.ts + // so we can easily adjust them for serverless where needed + timeouts: { + find: 10 * 1000, + try: 120 * 1000, + waitFor: 20 * 1000, + esRequestTimeout: 30 * 1000, + kibanaReportCompletion: 60 * 1000, + kibanaStabilize: 15 * 1000, + navigateStatusPageCheck: 250, + waitForExists: 2500, + }, + }; +} diff --git a/x-pack/test/api_integration/apis/slos/create_slo.ts b/x-pack/test/api_integration/apis/slos/create_slo.ts new file mode 100644 index 0000000000000..485c113705463 --- /dev/null +++ b/x-pack/test/api_integration/apis/slos/create_slo.ts @@ -0,0 +1,273 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import expect from '@kbn/expect'; +import type { CreateSLOInput } from '@kbn/slo-schema'; +import { SO_SLO_TYPE } from '@kbn/observability-plugin/server/saved_objects'; + +import { FtrProviderContext } from '../../ftr_provider_context'; +import { sloData } from './fixtures/create_slo'; + +export default function ({ getService }: FtrProviderContext) { + describe('Create SLOs', function () { + this.tags('skipCloud'); + + const supertestAPI = getService('supertest'); + const kibanaServer = getService('kibanaServer'); + const slo = getService('slo'); + + let createSLOInput: CreateSLOInput; + + before(async () => { + await slo.deleteAllSLOs(); + }); + + beforeEach(() => { + createSLOInput = sloData; + }); + + afterEach(async () => { + await slo.deleteAllSLOs(); + }); + + it('creates a new slo and transforms', async () => { + const apiResponse = await supertestAPI + .post('/api/observability/slos') + .set('kbn-xsrf', 'true') + .set('x-elastic-internal-origin', 'foo') + .send(createSLOInput) + .expect(200); + + expect(apiResponse.body).property('id'); + + const { id } = apiResponse.body; + + const savedObject = await kibanaServer.savedObjects.find({ + type: SO_SLO_TYPE, + }); + + expect(savedObject.saved_objects.length).eql(1); + + expect(savedObject.saved_objects[0].attributes).eql({ + budgetingMethod: 'occurrences', + updatedAt: savedObject.saved_objects[0].attributes.updatedAt, + createdAt: savedObject.saved_objects[0].attributes.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', + }, + tags: ['test'], + timeWindow: { + duration: '7d', + type: 'rolling', + }, + version: 2, + }); + + const rollUpTransformResponse = await supertestAPI + .get(`/internal/transform/transforms/slo-${id}-1`) + .set('kbn-xsrf', 'true') + .set('elastic-api-version', '1') + .send() + .expect(200); + + // expect roll up transform to be created + expect(rollUpTransformResponse.body).eql({ + count: 1, + transforms: [ + { + id: `slo-${id}-1`, + authorization: { roles: ['superuser'] }, + version: '10.0.0', + create_time: rollUpTransformResponse.body.transforms[0].create_time, + source: { + index: ['kbn-data-forge*'], + query: { + bool: { + filter: [ + { range: { '@timestamp': { gte: 'now-7d/d' } } }, + { + bool: { + should: [ + { + match: { + 'system.network.name': 'eth1', + }, + }, + ], + minimum_should_match: 1, + }, + }, + ], + }, + }, + runtime_mappings: { + 'slo.id': { + type: 'keyword', + script: { source: `emit('${id}')` }, + }, + 'slo.revision': { type: 'long', script: { source: 'emit(1)' } }, + }, + }, + dest: { + index: '.slo-observability.sli-v3', + pipeline: '.slo-observability.sli.pipeline-v3', + }, + frequency: '1m', + sync: { time: { field: '@timestamp', delay: '1m' } }, + pivot: { + group_by: { + 'slo.id': { terms: { field: 'slo.id' } }, + 'slo.revision': { terms: { field: 'slo.revision' } }, + 'slo.instanceId': { terms: { field: 'tags' } }, + 'slo.groupings.tags': { terms: { field: 'tags' } }, + '@timestamp': { date_histogram: { field: '@timestamp', fixed_interval: '1m' } }, + }, + aggregations: { + 'slo.numerator': { + filter: { + bool: { + should: [{ range: { 'container.cpu.user.pct': { lt: '1' } } }], + minimum_should_match: 1, + }, + }, + }, + 'slo.denominator': { + filter: { + bool: { + should: [{ exists: { field: 'container.cpu.user.pct' } }], + minimum_should_match: 1, + }, + }, + }, + }, + }, + description: `Rolled-up SLI data for SLO: Test SLO for api integration [id: ${id}, revision: 1]`, + settings: { deduce_mappings: false, unattended: true }, + _meta: { version: 3, managed: true, managed_by: 'observability' }, + }, + ], + }); + + const summaryTransform = await supertestAPI + .get(`/internal/transform/transforms/slo-summary-${id}-1`) + .set('kbn-xsrf', 'true') + .set('elastic-api-version', '1') + .send() + .expect(200); + + // expect summary transform to be created + expect(summaryTransform.body).eql({ + count: 1, + transforms: [ + { + id: `slo-summary-${id}-1`, + authorization: { roles: ['superuser'] }, + version: '10.0.0', + create_time: summaryTransform.body.transforms[0].create_time, + source: { + index: ['.slo-observability.sli-v3*'], + query: { + bool: { + filter: [ + { range: { '@timestamp': { gte: 'now-7d/m', lte: 'now/m' } } }, + { term: { 'slo.id': id } }, + { term: { 'slo.revision': 1 } }, + ], + }, + }, + }, + dest: { + index: '.slo-observability.summary-v3', + pipeline: `.slo-observability.summary.pipeline-${id}-1`, + }, + frequency: '1m', + sync: { time: { field: 'event.ingested', delay: '65s' } }, + pivot: { + group_by: { + 'slo.id': { terms: { field: 'slo.id' } }, + 'slo.revision': { terms: { field: 'slo.revision' } }, + 'slo.instanceId': { terms: { field: 'slo.instanceId' } }, + 'slo.groupings.tags': { + terms: { field: 'slo.groupings.tags' }, + }, + 'service.name': { terms: { field: 'service.name', missing_bucket: true } }, + 'service.environment': { + terms: { field: 'service.environment', missing_bucket: true }, + }, + 'transaction.name': { terms: { field: 'transaction.name', missing_bucket: true } }, + 'transaction.type': { terms: { field: 'transaction.type', missing_bucket: true } }, + }, + aggregations: { + goodEvents: { sum: { field: 'slo.numerator' } }, + totalEvents: { sum: { field: 'slo.denominator' } }, + sliValue: { + bucket_script: { + buckets_path: { goodEvents: 'goodEvents', totalEvents: 'totalEvents' }, + script: + 'if (params.totalEvents == 0) { return -1 } else if (params.goodEvents >= params.totalEvents) { return 1 } else { return params.goodEvents / params.totalEvents }', + }, + }, + errorBudgetInitial: { bucket_script: { buckets_path: {}, script: '1 - 0.99' } }, + errorBudgetConsumed: { + bucket_script: { + buckets_path: { + sliValue: 'sliValue', + errorBudgetInitial: 'errorBudgetInitial', + }, + script: + 'if (params.sliValue == -1) { return 0 } else { return (1 - params.sliValue) / params.errorBudgetInitial }', + }, + }, + errorBudgetRemaining: { + bucket_script: { + buckets_path: { errorBudgetConsumed: 'errorBudgetConsumed' }, + script: '1 - params.errorBudgetConsumed', + }, + }, + statusCode: { + bucket_script: { + buckets_path: { + sliValue: 'sliValue', + errorBudgetRemaining: 'errorBudgetRemaining', + }, + script: { + source: + 'if (params.sliValue == -1) { return 0 } else if (params.sliValue >= 0.99) { return 4 } else if (params.errorBudgetRemaining > 0) { return 2 } else { return 1 }', + }, + }, + }, + latestSliTimestamp: { max: { field: '@timestamp' } }, + }, + }, + description: `Summarise the rollup data of SLO: Test SLO for api integration [id: ${id}, revision: 1].`, + settings: { deduce_mappings: false, unattended: true }, + _meta: { version: 3, managed: true, managed_by: 'observability' }, + }, + ], + }); + }); + }); +} diff --git a/x-pack/test/api_integration/apis/slos/delete_slo.ts b/x-pack/test/api_integration/apis/slos/delete_slo.ts new file mode 100644 index 0000000000000..279f0e80464b2 --- /dev/null +++ b/x-pack/test/api_integration/apis/slos/delete_slo.ts @@ -0,0 +1,130 @@ +/* + * 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 } from '@kbn/infra-forge'; +import expect from '@kbn/expect'; +import type { CreateSLOInput } from '@kbn/slo-schema'; +import { SO_SLO_TYPE } from '@kbn/observability-plugin/server/saved_objects'; + +import { FtrProviderContext } from '../../ftr_provider_context'; +import { sloData } from './fixtures/create_slo'; +import { loadTestData } from './helper/load_test_data'; +import { SloEsClient } from './helper/es'; + +export default function ({ getService }: FtrProviderContext) { + describe('Delete SLOs', function () { + this.tags('skipCloud'); + + const supertestAPI = getService('supertest'); + const kibanaServer = getService('kibanaServer'); + const esClient = getService('es'); + const logger = getService('log'); + const slo = getService('slo'); + const retry = getService('retry'); + const sloEsClient = new SloEsClient(esClient); + + let createSLOInput: CreateSLOInput; + + before(async () => { + await slo.deleteAllSLOs(); + await sloEsClient.deleteTestSourceData(); + loadTestData(getService); + }); + + beforeEach(() => { + createSLOInput = sloData; + }); + + afterEach(async () => { + await slo.deleteAllSLOs(); + }); + + after(async () => { + await cleanup({ esClient, logger }); + await sloEsClient.deleteTestSourceData(); + }); + + it('deletes new slo saved object and transforms', async () => { + const id = await slo.create(createSLOInput); + + const savedObject = await kibanaServer.savedObjects.find({ + type: SO_SLO_TYPE, + }); + + expect(savedObject.saved_objects.length).eql(1); + + expect(savedObject.saved_objects[0].attributes.id).eql(id); + + await retry.tryForTime(300 * 1000, async () => { + // expect summary and rollup data to exist + const sloSummaryResponse = await sloEsClient.getSLOSummaryDataById(id); + const sloRollupResponse = await sloEsClient.getSLORollupDataById(id); + + expect(sloSummaryResponse.hits.hits.length > 0).eql(true); + expect(sloRollupResponse.hits.hits.length > 0).eql(true); + + const rollUpTransformResponse = await supertestAPI + .get(`/internal/transform/transforms/slo-${id}-1`) + .set('kbn-xsrf', 'true') + .set('elastic-api-version', '1') + .send() + .expect(200); + + // expect roll up transform to be created + expect(rollUpTransformResponse.body.transforms[0].id).eql(`slo-${id}-1`); + + const summaryTransform = await supertestAPI + .get(`/internal/transform/transforms/slo-summary-${id}-1`) + .set('kbn-xsrf', 'true') + .set('elastic-api-version', '1') + .send() + .expect(200); + + // expect summary transform to be created + expect(summaryTransform.body.transforms[0].id).eql(`slo-summary-${id}-1`); + + await supertestAPI + .delete(`/api/observability/slos/${id}`) + .set('kbn-xsrf', 'true') + .send() + .expect(204); + }); + + // await retry.tryForTime(150 * 1000, async () => { + const savedObjectAfterDelete = await kibanaServer.savedObjects.find({ + type: SO_SLO_TYPE, + }); + + // SO should now be deleted + expect(savedObjectAfterDelete.saved_objects.length).eql(0); + + // roll up transform should be deleted + await supertestAPI + .get(`/internal/transform/transforms/slo-${id}-1`) + .set('kbn-xsrf', 'true') + .set('elastic-api-version', '1') + .send() + .expect(404); + + // summary transform should be deleted + await supertestAPI + .get(`/internal/transform/transforms/slo-summary-${id}-1`) + .set('kbn-xsrf', 'true') + .set('elastic-api-version', '1') + .send() + .expect(404); + + // expect summary and rollup documents to be deleted + await retry.tryForTime(60 * 1000, async () => { + const sloSummaryResponseAfterDeletion = await sloEsClient.getSLOSummaryDataById(id); + const sloRollupResponseAfterDeletion = await sloEsClient.getSLORollupDataById(id); + expect(sloSummaryResponseAfterDeletion.hits.hits.length).eql(0); + // sometimes the ingest pipeline ingests one extra document after the transform is stopped + expect(sloRollupResponseAfterDeletion.hits.hits.length <= 1).eql(true); + }); + }); + }); +} diff --git a/x-pack/test/api_integration/apis/slos/fixtures/create_slo.ts b/x-pack/test/api_integration/apis/slos/fixtures/create_slo.ts new file mode 100644 index 0000000000000..e252acbb3d34f --- /dev/null +++ b/x-pack/test/api_integration/apis/slos/fixtures/create_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 sloData: 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/apis/slos/get_slo.ts b/x-pack/test/api_integration/apis/slos/get_slo.ts new file mode 100644 index 0000000000000..b121000d6033c --- /dev/null +++ b/x-pack/test/api_integration/apis/slos/get_slo.ts @@ -0,0 +1,465 @@ +/* + * 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 } from '@kbn/infra-forge'; +import expect from '@kbn/expect'; +import type { CreateSLOInput } from '@kbn/slo-schema'; + +import { FtrProviderContext } from '../../ftr_provider_context'; +import { loadTestData } from './helper/load_test_data'; +import { SloEsClient } from './helper/es'; +import { sloData } from './fixtures/create_slo'; + +export default function ({ getService }: FtrProviderContext) { + describe('Get SLOs', function () { + this.tags('skipCloud'); + + const supertestAPI = getService('supertest'); + const esClient = getService('es'); + const logger = getService('log'); + const retry = getService('retry'); + const slo = getService('slo'); + const sloEsClient = new SloEsClient(esClient); + + let createSLOInput: CreateSLOInput; + + const createSLO = async (requestOverrides?: Record) => { + return await slo.create({ + ...createSLOInput, + ...requestOverrides, + }); + }; + + before(async () => { + await slo.deleteAllSLOs(); + await sloEsClient.deleteTestSourceData(); + await loadTestData(getService); + }); + + beforeEach(async () => { + createSLOInput = sloData; + }); + + afterEach(async () => { + await slo.deleteAllSLOs(); + }); + + after(async () => { + await cleanup({ esClient, logger }); + await sloEsClient.deleteTestSourceData(); + }); + + it('gets slo by id and calculates SLI - occurances rolling', async () => { + const id = await createSLO({ + groupBy: '*', + }); + + await retry.tryForTime(300 * 1000, async () => { + const getResponse = await supertestAPI + .get(`/api/observability/slos/${id}`) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + expect(getResponse.body).eql({ + 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: '*', + id, + settings: { syncDelay: '1m', frequency: '1m' }, + revision: 1, + enabled: true, + createdAt: getResponse.body.createdAt, + updatedAt: getResponse.body.updatedAt, + version: 2, + instanceId: '*', + summary: { + sliValue: 0.5, + errorBudget: { + initial: 0.01, + consumed: 50, + remaining: -49, + isEstimated: false, + }, + status: 'VIOLATED', + }, + }); + }); + }); + + it('gets slo by id and calculates SLI - occurences calendarAligned', async () => { + const id = await createSLO({ + groupBy: '*', + timeWindow: { + duration: '1w', + type: 'calendarAligned', + }, + }); + + await retry.tryForTime(300 * 1000, async () => { + const getResponse = await supertestAPI + .get(`/api/observability/slos/${id}`) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + // expect summary transform to be created + expect(getResponse.body).eql({ + 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: '1w', type: 'calendarAligned' }, + objective: { target: 0.99 }, + tags: ['test'], + groupBy: '*', + id, + settings: { syncDelay: '1m', frequency: '1m' }, + revision: 1, + enabled: true, + createdAt: getResponse.body.createdAt, + updatedAt: getResponse.body.updatedAt, + version: 2, + instanceId: '*', + summary: { + sliValue: 0.5, + errorBudget: { + initial: 0.01, + consumed: 50, + remaining: -49, + isEstimated: true, + }, + status: 'VIOLATED', + }, + }); + }); + }); + + it('gets slo by id and calculates SLI - timeslices rolling', async () => { + const id = await createSLO({ + groupBy: '*', + timeWindow: { + duration: '7d', + type: 'rolling', + }, + budgetingMethod: 'timeslices', + objective: { + target: 0.99, + timesliceTarget: 0.95, + timesliceWindow: '1m', + }, + }); + + await retry.tryForTime(300 * 1000, async () => { + const getResponse = await supertestAPI + .get(`/api/observability/slos/${id}`) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + // expect summary transform to be created + expect(getResponse.body).eql({ + 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: 'timeslices', + timeWindow: { duration: '7d', type: 'rolling' }, + objective: { + target: 0.99, + timesliceTarget: 0.95, + timesliceWindow: '1m', + }, + tags: ['test'], + groupBy: '*', + id, + settings: { syncDelay: '1m', frequency: '1m' }, + revision: 1, + enabled: true, + createdAt: getResponse.body.createdAt, + updatedAt: getResponse.body.updatedAt, + version: 2, + instanceId: '*', + summary: { + sliValue: 0.5, + errorBudget: { + initial: 0.01, + consumed: 50, + remaining: -49, + isEstimated: false, + }, + status: 'VIOLATED', + }, + }); + }); + }); + + it('gets slo by id and calculates SLI - timeslices calendarAligned', async () => { + const id = await createSLO({ + groupBy: '*', + timeWindow: { + duration: '1w', + type: 'calendarAligned', + }, + budgetingMethod: 'timeslices', + objective: { + target: 0.99, + timesliceTarget: 0.95, + timesliceWindow: '10m', + }, + }); + + await retry.tryForTime(300 * 1000, async () => { + const getResponse = await supertestAPI + .get(`/api/observability/slos/${id}`) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + expect(getResponse.body).eql({ + 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: 'timeslices', + timeWindow: { duration: '1w', type: 'calendarAligned' }, + objective: { + target: 0.99, + timesliceTarget: 0.95, + timesliceWindow: '10m', + }, + tags: ['test'], + groupBy: '*', + id, + settings: { syncDelay: '1m', frequency: '1m' }, + revision: 1, + enabled: true, + createdAt: getResponse.body.createdAt, + updatedAt: getResponse.body.updatedAt, + version: 2, + instanceId: '*', + summary: { + sliValue: 0, + errorBudget: { + initial: 0.01, + consumed: 0.198413, + remaining: 0.801587, + isEstimated: false, + }, + status: 'DEGRADING', + }, + }); + }); + }); + + it('gets slos by query', async () => { + const id = await createSLO(); + await createSLO({ name: 'test int' }); + + await retry.tryForTime(300 * 1000, async () => { + const response = await supertestAPI + .get(`/api/observability/slos`) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + expect(response.body.results.length).eql(2); + + const searchResponse = await supertestAPI + .get(`/api/observability/slos?kqlQuery=slo.name%3Aapi*`) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + expect(searchResponse.body.results.length).eql(1); + + const searchResponse2 = await supertestAPI + .get(`/api/observability/slos?kqlQuery=slo.name%3Aint`) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + expect(searchResponse2.body.results.length).eql(1); + + const searchResponse3 = await supertestAPI + .get(`/api/observability/slos?kqlQuery=slo.name%3Aint*`) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + expect(searchResponse3.body.results.length).eql(2); + + const searchResponse4 = await supertestAPI + .get(`/api/observability/slos?kqlQuery=int*`) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + expect(searchResponse4.body.results.length).eql(2); + + const instanceResponse = await supertestAPI + .get(`/internal/observability/slos/${id}/_instances`) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + // expect 3 instances to be created + expect(instanceResponse.body.groupBy).eql('tags'); + expect(instanceResponse.body.instances.sort()).eql(['1', '2', '3']); + }); + }); + + it('gets slo definitions', async () => { + const id = await createSLO(); + const secondId = await createSLO({ name: 'test name int' }); + const response = await supertestAPI + .get(`/api/observability/slos/_definitions`) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + expect(response.body).eql({ + page: 1, + perPage: 100, + results: [ + { + budgetingMethod: 'occurrences', + createdAt: response.body.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', + }, + tags: ['test'], + timeWindow: { + duration: '7d', + type: 'rolling', + }, + updatedAt: response.body.results[0].updatedAt, + version: 2, + }, + { + budgetingMethod: 'occurrences', + createdAt: response.body.results[1].createdAt, + description: 'Fixture for api integration tests', + enabled: true, + groupBy: 'tags', + id: secondId, + 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 name int', + objective: { + target: 0.99, + }, + revision: 1, + settings: { + frequency: '1m', + syncDelay: '1m', + }, + tags: ['test'], + timeWindow: { + duration: '7d', + type: 'rolling', + }, + updatedAt: response.body.results[1].updatedAt, + version: 2, + }, + ], + total: 2, + }); + + // can search by name + const searchResponse = await supertestAPI + .get(`/api/observability/slos/_definitions?search=api`) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + expect(searchResponse.body.total).eql(1); + + const searchResponse2 = await supertestAPI + .get(`/api/observability/slos/_definitions?search=int`) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + expect(searchResponse2.body.total).eql(1); + + const searchResponse3 = await supertestAPI + .get(`/api/observability/slos/_definitions?search=int*`) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + expect(searchResponse3.body.total).eql(2); + }); + }); +} diff --git a/x-pack/test/api_integration/apis/slos/helper/es.ts b/x-pack/test/api_integration/apis/slos/helper/es.ts new file mode 100644 index 0000000000000..d1b50e625365a --- /dev/null +++ b/x-pack/test/api_integration/apis/slos/helper/es.ts @@ -0,0 +1,64 @@ +/* + * 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 { Client } from '@elastic/elasticsearch'; +import { + SLO_DESTINATION_INDEX_PATTERN, + SLO_SUMMARY_DESTINATION_INDEX_PATTERN, +} from '@kbn/observability-plugin/common/slo/constants'; + +export class SloEsClient { + constructor(private esClient: Client) {} + + public async getSLOSummaryDataById(id: string) { + return await this.esClient.search({ + index: SLO_SUMMARY_DESTINATION_INDEX_PATTERN, + body: { + query: { + bool: { + filter: [ + { + term: { 'slo.id': id }, + }, + { + term: { isTempDoc: false }, + }, + ], + }, + }, + }, + }); + } + + public async getSLORollupDataById(id: string) { + return await this.esClient.search({ + index: SLO_DESTINATION_INDEX_PATTERN, + body: { + query: { + bool: { + filter: [ + { + term: { 'slo.id': id }, + }, + ], + }, + }, + }, + }); + } + + public async deleteTestSourceData() { + try { + await this.esClient.deleteByQuery({ + index: 'kbn-data-forge-fake_hosts*', + query: { term: { 'system.network.name': 'eth1' } }, + }); + } catch (e) { + // eslint-disable-next-line no-console + console.warn('SLO api integration test data not found'); + } + } +} diff --git a/x-pack/test/api_integration/apis/slos/helper/load_test_data.ts b/x-pack/test/api_integration/apis/slos/helper/load_test_data.ts new file mode 100644 index 0000000000000..d1558a1357a91 --- /dev/null +++ b/x-pack/test/api_integration/apis/slos/helper/load_test_data.ts @@ -0,0 +1,23 @@ +/* + * 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 { generate } from '@kbn/infra-forge'; +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export async function loadTestData(getService: FtrProviderContext['getService']) { + const DATE_VIEW = 'kbn-data-forge-fake_hosts'; + const DATA_VIEW_ID = 'data-view-id'; + const dataViewApi = getService('dataViewApi'); + const esClient = getService('es'); + const logger = getService('log'); + + await generate({ esClient, lookback: 'now-16m', logger }); + await dataViewApi.create({ + name: DATE_VIEW, + id: DATA_VIEW_ID, + title: DATE_VIEW, + }); +} diff --git a/x-pack/test/api_integration/apis/slos/index.ts b/x-pack/test/api_integration/apis/slos/index.ts new file mode 100644 index 0000000000000..6276dd4a4cf6f --- /dev/null +++ b/x-pack/test/api_integration/apis/slos/index.ts @@ -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. + */ + +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('SLO API Tests', () => { + loadTestFile(require.resolve('./create_slo')); + loadTestFile(require.resolve('./delete_slo')); + loadTestFile(require.resolve('./get_slo')); + loadTestFile(require.resolve('./update_slo')); + loadTestFile(require.resolve('./reset_slo')); + }); +} diff --git a/x-pack/test/api_integration/apis/slos/reset_slo.ts b/x-pack/test/api_integration/apis/slos/reset_slo.ts new file mode 100644 index 0000000000000..37dfbda0a882f --- /dev/null +++ b/x-pack/test/api_integration/apis/slos/reset_slo.ts @@ -0,0 +1,105 @@ +/* + * 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 } from '@kbn/infra-forge'; +import expect from '@kbn/expect'; +import { SO_SLO_TYPE } from '@kbn/observability-plugin/server/saved_objects'; + +import { FtrProviderContext } from '../../ftr_provider_context'; +import { loadTestData } from './helper/load_test_data'; +import { SloEsClient } from './helper/es'; + +export default function ({ getService }: FtrProviderContext) { + describe('Reset SLOs', function () { + this.tags('skipCloud'); + + const supertestAPI = getService('supertest'); + const kibanaServer = getService('kibanaServer'); + const esClient = getService('es'); + const logger = getService('log'); + const slo = getService('slo'); + const sloEsClient = new SloEsClient(esClient); + + before(async () => { + await sloEsClient.deleteTestSourceData(); + await slo.deleteAllSLOs(); + await loadTestData(getService); + }); + + afterEach(async () => { + await slo.deleteAllSLOs(); + }); + + after(async () => { + await cleanup({ esClient, logger }); + await sloEsClient.deleteTestSourceData(); + }); + + it('updates the SO and transforms', async () => { + // create mock old SLO + const id = 'bdaeccdd-dc63-4138-a1d5-92c075f88087'; + await kibanaServer.savedObjects.clean({ + types: [SO_SLO_TYPE], + }); + await kibanaServer.savedObjects.create({ + type: SO_SLO_TYPE, + overwrite: true, + id, + attributes: { + 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: '*', + id, + settings: { + syncDelay: '1m', + frequency: '1m', + }, + revision: 1, + enabled: true, + createdAt: '2023-12-14T01:12:35.638Z', + updatedAt: '2023-12-14T01:12:35.638Z', + version: 1, + }, + }); + + const responseBeforeReset = await supertestAPI + .get(`/api/observability/slos/_definitions`) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + expect(responseBeforeReset.body.results[0].version).eql(1); + + await supertestAPI + .post(`/api/observability/slos/${id}/_reset`) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + const responseAfterReset = await supertestAPI + .get(`/api/observability/slos/_definitions`) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + expect(responseAfterReset.body.results[0].version).eql(2); + }); + }); +} diff --git a/x-pack/test/api_integration/apis/slos/update_slo.ts b/x-pack/test/api_integration/apis/slos/update_slo.ts new file mode 100644 index 0000000000000..3d7e20baa32ff --- /dev/null +++ b/x-pack/test/api_integration/apis/slos/update_slo.ts @@ -0,0 +1,677 @@ +/* + * 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 } from '@kbn/infra-forge'; +import expect from '@kbn/expect'; +import type { CreateSLOInput } from '@kbn/slo-schema'; +import { SO_SLO_TYPE } from '@kbn/observability-plugin/server/saved_objects'; + +import { FtrProviderContext } from '../../ftr_provider_context'; +import { loadTestData } from './helper/load_test_data'; +import { sloData } from './fixtures/create_slo'; + +export default function ({ getService }: FtrProviderContext) { + describe('Update SLOs', function () { + this.tags('skipCloud'); + + const supertestAPI = getService('supertest'); + const kibanaServer = getService('kibanaServer'); + const esClient = getService('es'); + const logger = getService('log'); + const slo = getService('slo'); + + let createSLOInput: CreateSLOInput; + + before(async () => { + await slo.deleteAllSLOs(); + await loadTestData(getService); + }); + + beforeEach(() => { + createSLOInput = sloData; + }); + + afterEach(async () => { + await slo.deleteAllSLOs(); + }); + + after(async () => { + await cleanup({ esClient, logger }); + }); + + it('updates the SO and transforms', async () => { + const apiResponse = await supertestAPI + .post('/api/observability/slos') + .set('kbn-xsrf', 'true') + .send(createSLOInput) + .expect(200); + + expect(apiResponse.body).property('id'); + + const { id } = apiResponse.body; + + await supertestAPI + .put(`/api/observability/slos/${id}`) + .set('kbn-xsrf', 'true') + .send({ + ...createSLOInput, + groupBy: 'hosts', + }) + .expect(200); + + const savedObject = await kibanaServer.savedObjects.find({ + type: SO_SLO_TYPE, + }); + + expect(savedObject.saved_objects.length).eql(1); + + expect(savedObject.saved_objects[0].attributes).eql({ + budgetingMethod: 'occurrences', + updatedAt: savedObject.saved_objects[0].attributes.updatedAt, + createdAt: savedObject.saved_objects[0].attributes.createdAt, + description: 'Fixture for api integration tests', + enabled: true, + groupBy: 'hosts', + 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: 2, + settings: { + frequency: '1m', + syncDelay: '1m', + }, + tags: ['test'], + timeWindow: { + duration: '7d', + type: 'rolling', + }, + version: 2, + }); + + const rollUpTransformResponse = await supertestAPI + .get(`/internal/transform/transforms/slo-${id}-2`) + .set('kbn-xsrf', 'true') + .set('elastic-api-version', '1') + .send() + .expect(200); + + // expect roll up transform to be created + expect(rollUpTransformResponse.body).eql({ + count: 1, + transforms: [ + { + id: `slo-${id}-2`, + authorization: { roles: ['superuser'] }, + version: '10.0.0', + create_time: rollUpTransformResponse.body.transforms[0].create_time, + source: { + index: ['kbn-data-forge*'], + query: { + bool: { + filter: [ + { range: { '@timestamp': { gte: 'now-7d/d' } } }, + { + bool: { + should: [ + { + match: { + 'system.network.name': 'eth1', + }, + }, + ], + minimum_should_match: 1, + }, + }, + ], + }, + }, + runtime_mappings: { + 'slo.id': { + type: 'keyword', + script: { source: `emit('${id}')` }, + }, + 'slo.revision': { type: 'long', script: { source: 'emit(2)' } }, + }, + }, + dest: { + index: '.slo-observability.sli-v3', + pipeline: '.slo-observability.sli.pipeline-v3', + }, + frequency: '1m', + sync: { time: { field: '@timestamp', delay: '1m' } }, + pivot: { + group_by: { + 'slo.id': { terms: { field: 'slo.id' } }, + 'slo.revision': { terms: { field: 'slo.revision' } }, + 'slo.instanceId': { terms: { field: 'hosts' } }, + 'slo.groupings.hosts': { terms: { field: 'hosts' } }, + '@timestamp': { date_histogram: { field: '@timestamp', fixed_interval: '1m' } }, + }, + aggregations: { + 'slo.numerator': { + filter: { + bool: { + should: [{ range: { 'container.cpu.user.pct': { lt: '1' } } }], + minimum_should_match: 1, + }, + }, + }, + 'slo.denominator': { + filter: { + bool: { + should: [{ exists: { field: 'container.cpu.user.pct' } }], + minimum_should_match: 1, + }, + }, + }, + }, + }, + description: `Rolled-up SLI data for SLO: Test SLO for api integration [id: ${id}, revision: 2]`, + settings: { deduce_mappings: false, unattended: true }, + _meta: { version: 3, managed: true, managed_by: 'observability' }, + }, + ], + }); + + const summaryTransform = await supertestAPI + .get(`/internal/transform/transforms/slo-summary-${id}-2`) + .set('kbn-xsrf', 'true') + .set('elastic-api-version', '1') + .send() + .expect(200); + + // expect summary transform to be created + expect(summaryTransform.body).eql({ + count: 1, + transforms: [ + { + id: `slo-summary-${id}-2`, + authorization: { roles: ['superuser'] }, + version: '10.0.0', + create_time: summaryTransform.body.transforms[0].create_time, + source: { + index: ['.slo-observability.sli-v3*'], + query: { + bool: { + filter: [ + { range: { '@timestamp': { gte: 'now-7d/m', lte: 'now/m' } } }, + { term: { 'slo.id': id } }, + { term: { 'slo.revision': 2 } }, + ], + }, + }, + }, + dest: { + index: '.slo-observability.summary-v3', + pipeline: `.slo-observability.summary.pipeline-${id}-2`, + }, + frequency: '1m', + sync: { time: { field: 'event.ingested', delay: '65s' } }, + pivot: { + group_by: { + 'slo.id': { terms: { field: 'slo.id' } }, + 'slo.revision': { terms: { field: 'slo.revision' } }, + 'slo.instanceId': { terms: { field: 'slo.instanceId' } }, + 'slo.groupings.hosts': { + terms: { field: 'slo.groupings.hosts' }, + }, + 'service.name': { terms: { field: 'service.name', missing_bucket: true } }, + 'service.environment': { + terms: { field: 'service.environment', missing_bucket: true }, + }, + 'transaction.name': { terms: { field: 'transaction.name', missing_bucket: true } }, + 'transaction.type': { terms: { field: 'transaction.type', missing_bucket: true } }, + }, + aggregations: { + goodEvents: { sum: { field: 'slo.numerator' } }, + totalEvents: { sum: { field: 'slo.denominator' } }, + sliValue: { + bucket_script: { + buckets_path: { goodEvents: 'goodEvents', totalEvents: 'totalEvents' }, + script: + 'if (params.totalEvents == 0) { return -1 } else if (params.goodEvents >= params.totalEvents) { return 1 } else { return params.goodEvents / params.totalEvents }', + }, + }, + errorBudgetInitial: { bucket_script: { buckets_path: {}, script: '1 - 0.99' } }, + errorBudgetConsumed: { + bucket_script: { + buckets_path: { + sliValue: 'sliValue', + errorBudgetInitial: 'errorBudgetInitial', + }, + script: + 'if (params.sliValue == -1) { return 0 } else { return (1 - params.sliValue) / params.errorBudgetInitial }', + }, + }, + errorBudgetRemaining: { + bucket_script: { + buckets_path: { errorBudgetConsumed: 'errorBudgetConsumed' }, + script: '1 - params.errorBudgetConsumed', + }, + }, + statusCode: { + bucket_script: { + buckets_path: { + sliValue: 'sliValue', + errorBudgetRemaining: 'errorBudgetRemaining', + }, + script: { + source: + 'if (params.sliValue == -1) { return 0 } else if (params.sliValue >= 0.99) { return 4 } else if (params.errorBudgetRemaining > 0) { return 2 } else { return 1 }', + }, + }, + }, + latestSliTimestamp: { max: { field: '@timestamp' } }, + }, + }, + description: `Summarise the rollup data of SLO: Test SLO for api integration [id: ${id}, revision: 2].`, + settings: { deduce_mappings: false, unattended: true }, + _meta: { version: 3, managed: true, managed_by: 'observability' }, + }, + ], + }); + }); + + it('updates an existing slo and does not update transforms when relevant fields are changed', async () => { + const request = createSLOInput; + + const apiResponse = await supertestAPI + .post('/api/observability/slos') + .set('kbn-xsrf', 'true') + .send(request) + .expect(200); + + expect(apiResponse.body).property('id'); + + const { id } = apiResponse.body; + + const savedObject = await kibanaServer.savedObjects.find({ + type: SO_SLO_TYPE, + }); + + expect(savedObject.saved_objects.length).eql(1); + + await supertestAPI + .get(`/internal/transform/transforms/slo-${id}-1`) + .set('kbn-xsrf', 'true') + .set('elastic-api-version', '1') + .send() + .expect(200); + + await supertestAPI + .get(`/internal/transform/transforms/slo-summary-${id}-1`) + .set('kbn-xsrf', 'true') + .set('elastic-api-version', '1') + .send() + .expect(200); + + // change name + await supertestAPI + .put(`/api/observability/slos/${id}`) + .set('kbn-xsrf', 'true') + .send({ + ...request, + name: 'test name', + }) + .expect(200); + + await supertestAPI + .get(`/internal/transform/transforms/slo-${id}-1`) + .set('kbn-xsrf', 'true') + .set('elastic-api-version', '1') + .send() + .expect(200); + + await supertestAPI + .get(`/internal/transform/transforms/slo-summary-${id}-1`) + .set('kbn-xsrf', 'true') + .set('elastic-api-version', '1') + .send() + .expect(200); + + // change description + await supertestAPI + .put(`/api/observability/slos/${id}`) + .set('kbn-xsrf', 'true') + .send({ + ...request, + description: 'test description', + }) + .expect(200); + + await supertestAPI + .get(`/internal/transform/transforms/slo-${id}-1`) + .set('kbn-xsrf', 'true') + .set('elastic-api-version', '1') + .send() + .expect(200); + + await supertestAPI + .get(`/internal/transform/transforms/slo-summary-${id}-1`) + .set('kbn-xsrf', 'true') + .set('elastic-api-version', '1') + .send() + .expect(200); + + // change tags + await supertestAPI + .put(`/api/observability/slos/${id}`) + .set('kbn-xsrf', 'true') + .send({ + ...request, + tags: ['testTag'], + }) + .expect(200); + + await supertestAPI + .get(`/internal/transform/transforms/slo-${id}-1`) + .set('kbn-xsrf', 'true') + .set('elastic-api-version', '1') + .send() + .expect(200); + + await supertestAPI + .get(`/internal/transform/transforms/slo-summary-${id}-1`) + .set('kbn-xsrf', 'true') + .set('elastic-api-version', '1') + .send() + .expect(200); + }); + + it('updates an existing slo and updates transforms when relevant fields are changed', async () => { + const request = createSLOInput; + + const apiResponse = await supertestAPI + .post('/api/observability/slos') + .set('kbn-xsrf', 'true') + .send(request) + .expect(200); + + expect(apiResponse.body).property('id'); + + const { id } = apiResponse.body; + + const savedObject = await kibanaServer.savedObjects.find({ + type: SO_SLO_TYPE, + }); + + expect(savedObject.saved_objects.length).eql(1); + + await supertestAPI + .get(`/internal/transform/transforms/slo-${id}-1`) + .set('kbn-xsrf', 'true') + .set('elastic-api-version', '1') + .send() + .expect(200); + + await supertestAPI + .get(`/internal/transform/transforms/slo-summary-${id}-1`) + .set('kbn-xsrf', 'true') + .set('elastic-api-version', '1') + .send() + .expect(200); + + // change group by + await supertestAPI + .put(`/api/observability/slos/${id}`) + .set('kbn-xsrf', 'true') + .send({ + ...request, + groupBy: 'hosts', + }) + .expect(200); + + await supertestAPI + .get(`/internal/transform/transforms/slo-${id}-1`) + .set('kbn-xsrf', 'true') + .set('elastic-api-version', '1') + .send() + .expect(404); + + await supertestAPI + .get(`/internal/transform/transforms/slo-summary-${id}-1`) + .set('kbn-xsrf', 'true') + .set('elastic-api-version', '1') + .send() + .expect(404); + + await supertestAPI + .get(`/internal/transform/transforms/slo-${id}-2`) + .set('kbn-xsrf', 'true') + .set('elastic-api-version', '1') + .send() + .expect(200); + + await supertestAPI + .get(`/internal/transform/transforms/slo-summary-${id}-2`) + .set('kbn-xsrf', 'true') + .set('elastic-api-version', '1') + .send() + .expect(200); + + // change indicator + await supertestAPI + .put(`/api/observability/slos/${id}`) + .set('kbn-xsrf', 'true') + .send({ + ...request, + indicator: { + ...request.indicator, + params: { + ...request.indicator.params, + index: 'test-index-*', + }, + }, + }) + .expect(200); + + await supertestAPI + .get(`/internal/transform/transforms/slo-${id}-2`) + .set('kbn-xsrf', 'true') + .set('elastic-api-version', '1') + .send() + .expect(404); + + await supertestAPI + .get(`/internal/transform/transforms/slo-summary-${id}-2`) + .set('kbn-xsrf', 'true') + .set('elastic-api-version', '1') + .send() + .expect(404); + + await supertestAPI + .get(`/internal/transform/transforms/slo-${id}-3`) + .set('kbn-xsrf', 'true') + .set('elastic-api-version', '1') + .send() + .expect(200); + + await supertestAPI + .get(`/internal/transform/transforms/slo-summary-${id}-3`) + .set('kbn-xsrf', 'true') + .set('elastic-api-version', '1') + .send() + .expect(200); + + // change time window + await supertestAPI + .put(`/api/observability/slos/${id}`) + .set('kbn-xsrf', 'true') + .send({ + ...request, + timeWindow: { + ...request.timeWindow, + duration: '7d', + }, + }) + .expect(200); + + await supertestAPI + .get(`/internal/transform/transforms/slo-${id}-3`) + .set('kbn-xsrf', 'true') + .set('elastic-api-version', '1') + .send() + .expect(404); + + await supertestAPI + .get(`/internal/transform/transforms/slo-summary-${id}-3`) + .set('kbn-xsrf', 'true') + .set('elastic-api-version', '1') + .send() + .expect(404); + + await supertestAPI + .get(`/internal/transform/transforms/slo-${id}-4`) + .set('kbn-xsrf', 'true') + .set('elastic-api-version', '1') + .send() + .expect(200); + + await supertestAPI + .get(`/internal/transform/transforms/slo-summary-${id}-4`) + .set('kbn-xsrf', 'true') + .set('elastic-api-version', '1') + .send() + .expect(200); + + // change objective + await supertestAPI + .put(`/api/observability/slos/${id}`) + .set('kbn-xsrf', 'true') + .send({ + ...request, + objective: { + target: 0.97, + }, + }) + .expect(200); + + await supertestAPI + .get(`/internal/transform/transforms/slo-${id}-4`) + .set('kbn-xsrf', 'true') + .set('elastic-api-version', '1') + .send() + .expect(404); + + await supertestAPI + .get(`/internal/transform/transforms/slo-summary-${id}-4`) + .set('kbn-xsrf', 'true') + .set('elastic-api-version', '1') + .send() + .expect(404); + + await supertestAPI + .get(`/internal/transform/transforms/slo-${id}-5`) + .set('kbn-xsrf', 'true') + .set('elastic-api-version', '1') + .send() + .expect(200); + + await supertestAPI + .get(`/internal/transform/transforms/slo-summary-${id}-5`) + .set('kbn-xsrf', 'true') + .set('elastic-api-version', '1') + .send() + .expect(200); + + // change budgetingMethod + await supertestAPI + .put(`/api/observability/slos/${id}`) + .set('kbn-xsrf', 'true') + .send({ + ...request, + budgetingMethod: 'timeslices', + objective: { + target: 0.99, + timesliceTarget: 0.95, + timesliceWindow: '1m', + }, + }) + .expect(200); + + await supertestAPI + .get(`/internal/transform/transforms/slo-${id}-5`) + .set('kbn-xsrf', 'true') + .set('elastic-api-version', '1') + .send() + .expect(404); + + await supertestAPI + .get(`/internal/transform/transforms/slo-summary-${id}-5`) + .set('kbn-xsrf', 'true') + .set('elastic-api-version', '1') + .send() + .expect(404); + + await supertestAPI + .get(`/internal/transform/transforms/slo-${id}-6`) + .set('kbn-xsrf', 'true') + .set('elastic-api-version', '1') + .send() + .expect(200); + + await supertestAPI + .get(`/internal/transform/transforms/slo-summary-${id}-6`) + .set('kbn-xsrf', 'true') + .set('elastic-api-version', '1') + .send() + .expect(200); + + // change settings + await supertestAPI + .put(`/api/observability/slos/${id}`) + .set('kbn-xsrf', 'true') + .send({ + ...request, + settings: { + frequency: '2m', + syncDelay: '5m', + }, + }) + .expect(200); + + await supertestAPI + .get(`/internal/transform/transforms/slo-${id}-6`) + .set('kbn-xsrf', 'true') + .set('elastic-api-version', '1') + .send() + .expect(404); + + await supertestAPI + .get(`/internal/transform/transforms/slo-summary-${id}-6`) + .set('kbn-xsrf', 'true') + .set('elastic-api-version', '1') + .send() + .expect(404); + + await supertestAPI + .get(`/internal/transform/transforms/slo-${id}-7`) + .set('kbn-xsrf', 'true') + .set('elastic-api-version', '1') + .send() + .expect(200); + + await supertestAPI + .get(`/internal/transform/transforms/slo-summary-${id}-7`) + .set('kbn-xsrf', 'true') + .set('elastic-api-version', '1') + .send() + .expect(200); + }); + }); +} diff --git a/x-pack/test/api_integration/services/data_view_api.ts b/x-pack/test/api_integration/services/data_view_api.ts new file mode 100644 index 0000000000000..430ff60aac1e2 --- /dev/null +++ b/x-pack/test/api_integration/services/data_view_api.ts @@ -0,0 +1,52 @@ +/* + * 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 { FtrProviderContext } from '../ftr_provider_context'; + +export function DataViewApiProvider({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + + return { + async create({ id, name, title }: { id: string; name: string; title: string }) { + const { body } = await supertest + .post(`/api/content_management/rpc/create`) + .set('kbn-xsrf', 'foo') + .set('x-elastic-internal-origin', 'foo') + .send({ + contentTypeId: 'index-pattern', + data: { + fieldAttrs: '{}', + title, + timeFieldName: '@timestamp', + sourceFilters: '[]', + fields: '[]', + fieldFormatMap: '{}', + typeMeta: '{}', + runtimeFieldMap: '{}', + name, + }, + options: { id }, + version: 1, + }); + return body; + }, + + async delete({ id }: { id: string }) { + const { body } = await supertest + .post(`/api/content_management/rpc/delete`) + .set('kbn-xsrf', 'foo') + .set('x-elastic-internal-origin', 'foo') + .send({ + contentTypeId: 'index-pattern', + id, + options: { force: true }, + version: 1, + }); + return body; + }, + }; +} diff --git a/x-pack/test/api_integration/services/index.ts b/x-pack/test/api_integration/services/index.ts index 1f8d4576d908d..edfaf5b668d2e 100644 --- a/x-pack/test/api_integration/services/index.ts +++ b/x-pack/test/api_integration/services/index.ts @@ -22,6 +22,8 @@ import { IngestManagerProvider } from '../../common/services/ingest_manager'; import { TransformProvider } from './transform'; import { IngestPipelinesProvider } from './ingest_pipelines'; import { IndexManagementProvider } from './index_management'; +import { DataViewApiProvider } from './data_view_api'; +import { SloApiProvider } from './slo'; export const services = { ...commonServices, @@ -30,6 +32,7 @@ export const services = { supertest: kibanaApiIntegrationServices.supertest, aiops: AiopsProvider, + dataViewApi: DataViewApiProvider, esSupertestWithoutAuth: EsSupertestWithoutAuthProvider, infraOpsSourceConfiguration: InfraOpsSourceConfigurationProvider, supertestWithoutAuth: SupertestWithoutAuthProvider, @@ -39,4 +42,5 @@ export const services = { transform: TransformProvider, ingestPipelines: IngestPipelinesProvider, indexManagement: IndexManagementProvider, + slo: SloApiProvider, }; diff --git a/x-pack/test/api_integration/services/slo.ts b/x-pack/test/api_integration/services/slo.ts new file mode 100644 index 0000000000000..c6d400d2452ce --- /dev/null +++ b/x-pack/test/api_integration/services/slo.ts @@ -0,0 +1,51 @@ +/* + * 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, FindSLODefinitionsResponse } from '@kbn/slo-schema'; +import { FtrProviderContext } from '../ftr_provider_context'; + +export function SloApiProvider({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + + return { + async create(params: CreateSLOInput) { + const slo = await supertest + .post('/api/observability/slos') + .set('kbn-xsrf', 'true') + .send(params) + .expect(200); + + const { id } = slo.body; + + const reqBody = [{ id: `slo-${id}-1` }, { id: `slo-summary-${id}-1` }]; + await supertest + .post(`/internal/transform/schedule_now_transforms`) + .set('kbn-xsrf', 'true') + .set('elastic-api-version', '1') + .send(reqBody) + .expect(200); + + return id; + }, + async deleteAllSLOs() { + const response = await supertest + .get(`/api/observability/slos/_definitions`) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + await Promise.all( + (response.body as FindSLODefinitionsResponse).results.map(({ id }) => { + return supertest + .delete(`/api/observability/slos/${id}`) + .set('kbn-xsrf', 'true') + .send() + .expect(204); + }) + ); + }, + }; +} diff --git a/x-pack/test/tsconfig.json b/x-pack/test/tsconfig.json index 4ca836a475546..354e6819e3674 100644 --- a/x-pack/test/tsconfig.json +++ b/x-pack/test/tsconfig.json @@ -164,6 +164,7 @@ "@kbn/io-ts-utils", "@kbn/log-explorer-plugin", "@kbn/security-plugin-types-common", + "@kbn/slo-schema", "@kbn/typed-react-router-config", "@kbn/ftr-common-functional-ui-services", ]