diff --git a/x-pack/test_serverless/api_integration/services/slo_api.ts b/x-pack/test_serverless/api_integration/services/slo_api.ts index 96b7f9e518f31..c7403d2708429 100644 --- a/x-pack/test_serverless/api_integration/services/slo_api.ts +++ b/x-pack/test_serverless/api_integration/services/slo_api.ts @@ -6,8 +6,11 @@ */ import { + CreateSLOInput, fetchHistoricalSummaryParamsSchema, FetchHistoricalSummaryResponse, + FindSLODefinitionsResponse, + UpdateSLOInput, } from '@kbn/slo-schema'; import * as t from 'io-ts'; import type { RoleCredentials } from '../../shared/services'; @@ -40,30 +43,6 @@ export interface SloBurnRateRuleParams { dependencies?: Dependency[]; } -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 >; @@ -71,14 +50,15 @@ type FetchHistoricalSummaryParams = t.OutputOf< export function SloApiProvider({ getService }: FtrProviderContext) { const es = getService('es'); const supertest = getService('supertest'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); const svlCommonApi = getService('svlCommonApi'); const retry = getService('retry'); const requestTimeout = 30 * 1000; const retryTimeout = 180 * 1000; return { - async create(slo: SloParams, roleAuthc: RoleCredentials) { - const { body } = await supertest + async create(slo: CreateSLOInput, roleAuthc: RoleCredentials) { + const { body } = await supertestWithoutAuth .post(`/api/observability/slos`) .set(svlCommonApi.getInternalRequestHeader()) .set(roleAuthc.apiKeyHeader) @@ -87,19 +67,43 @@ export function SloApiProvider({ getService }: FtrProviderContext) { return body; }, + async update( + { sloId, slo }: { sloId: string; slo: UpdateSLOInput }, + roleAuthc: RoleCredentials + ) { + const { body } = await supertestWithoutAuth + .put(`/api/observability/slos/${sloId}`) + .set(svlCommonApi.getInternalRequestHeader()) + .set(roleAuthc.apiKeyHeader) + .send(slo); + + return body; + }, + async delete({ sloId, roleAuthc }: { sloId: string; roleAuthc: RoleCredentials }) { - const response = await supertest + const response = await supertestWithoutAuth .delete(`/api/observability/slos/${sloId}`) .set(svlCommonApi.getInternalRequestHeader()) .set(roleAuthc.apiKeyHeader); return response; }, + async findDefinitions(roleAuthc: RoleCredentials): Promise { + const { body } = await supertestWithoutAuth + .get(`/api/observability/slos/_definitions`) + .set(svlCommonApi.getInternalRequestHeader()) + .set(roleAuthc.apiKeyHeader) + .send() + .expect(200); + + return body; + }, + async fetchHistoricalSummary( params: FetchHistoricalSummaryParams, roleAuthc: RoleCredentials ): Promise { - const { body } = await supertest + const { body } = await supertestWithoutAuth .post(`/internal/observability/slos/_historical_summary`) .set(svlCommonApi.getInternalRequestHeader()) .set(roleAuthc.apiKeyHeader) @@ -119,7 +123,7 @@ export function SloApiProvider({ getService }: FtrProviderContext) { throw new Error(`sloId is undefined`); } return await retry.tryForTime(retryTimeout, async () => { - const response = await supertest + const response = await supertestWithoutAuth .delete(`/api/observability/slos/${sloId}`) .set(svlCommonApi.getInternalRequestHeader()) .set(roleAuthc.apiKeyHeader) @@ -133,10 +137,10 @@ export function SloApiProvider({ getService }: FtrProviderContext) { async waitForSloCreated({ sloId, roleAuthc }: { sloId: string; roleAuthc: RoleCredentials }) { if (!sloId) { - throw new Error(`'sloId is undefined`); + throw new Error(`sloId is undefined`); } return await retry.tryForTime(retryTimeout, async () => { - const response = await supertest + const response = await supertestWithoutAuth .get(`/api/observability/slos/${sloId}`) .set(svlCommonApi.getInternalRequestHeader()) .set(roleAuthc.apiKeyHeader) @@ -148,6 +152,23 @@ export function SloApiProvider({ getService }: FtrProviderContext) { }); }, + async waitForSloReseted(sloId: string, roleAuthc: RoleCredentials) { + if (!sloId) { + throw new Error('sloId is undefined'); + } + return await retry.tryForTime(retryTimeout, async () => { + const response = await supertestWithoutAuth + .post(`/api/observability/slos/${sloId}/_reset`) + .set(svlCommonApi.getInternalRequestHeader()) + .set(roleAuthc.apiKeyHeader) + .timeout(requestTimeout); + if (response.body.id === undefined) { + throw new Error(`Error reseting ${sloId}`); + } + return response.body; + }); + }, + async waitForSloSummaryTempIndexToExist(index: string) { if (!index) { throw new Error(`index is undefined`); @@ -193,6 +214,7 @@ export function SloApiProvider({ getService }: FtrProviderContext) { return response; }); }, + async deleteAllSLOs() { const response = await supertest .get(`/api/observability/slos/_definitions`) diff --git a/x-pack/test_serverless/api_integration/services/transform/api.ts b/x-pack/test_serverless/api_integration/services/transform/api.ts index b96f8aeca2ad9..84fdd2451cbaa 100644 --- a/x-pack/test_serverless/api_integration/services/transform/api.ts +++ b/x-pack/test_serverless/api_integration/services/transform/api.ts @@ -267,7 +267,15 @@ export function TransformAPIProvider({ getService }: FtrProviderContext) { async stopTransform(transformId: string) { log.debug(`Stopping transform '${transformId}' ...`); - const { body, status } = await esSupertest.post(`/_transform/${transformId}/_stop`); + const { body, status } = await esSupertest.post( + `/_transform/${transformId}/_stop?force=true` + ); + this.assertResponseStatusCode(200, status, body); + }, + + async deleteTransform(transformId: string) { + log.debug(`Deleting transform '${transformId}' ...`); + const { body, status } = await esSupertest.delete(`/_transform/${transformId}`); this.assertResponseStatusCode(200, status, body); }, diff --git a/x-pack/test_serverless/api_integration/test_suites/observability/slos/index.ts b/x-pack/test_serverless/api_integration/test_suites/observability/slos/index.ts index 8df59e6f3b624..0256db73bccbb 100644 --- a/x-pack/test_serverless/api_integration/test_suites/observability/slos/index.ts +++ b/x-pack/test_serverless/api_integration/test_suites/observability/slos/index.ts @@ -9,7 +9,9 @@ import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('SLOs', function () { loadTestFile(require.resolve('./create_slo')); + loadTestFile(require.resolve('./update_slo')); loadTestFile(require.resolve('./delete_slo')); + loadTestFile(require.resolve('./reset_slo')); loadTestFile(require.resolve('./fetch_historical_summary')); }); } diff --git a/x-pack/test_serverless/api_integration/test_suites/observability/slos/reset_slo.ts b/x-pack/test_serverless/api_integration/test_suites/observability/slos/reset_slo.ts new file mode 100644 index 0000000000000..c5fc8972ca66a --- /dev/null +++ b/x-pack/test_serverless/api_integration/test_suites/observability/slos/reset_slo.ts @@ -0,0 +1,133 @@ +/* + * 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 { cleanup, generate } from '@kbn/infra-forge'; +import { getSLOPipelineId, getSLOSummaryPipelineId } from '@kbn/slo-plugin/common/constants'; +import { SO_SLO_TYPE } from '@kbn/slo-plugin/server/saved_objects'; +import { ALL_VALUE } from '@kbn/slo-schema'; +import type { RoleCredentials } from '../../../../shared/services'; +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ getService }: FtrProviderContext) { + const esClient = getService('es'); + const supertest = getService('supertest'); + + const esDeleteAllIndices = getService('esDeleteAllIndices'); + const logger = getService('log'); + const dataViewApi = getService('dataViewApi'); + const sloApi = getService('sloApi'); + const kibanaServer = getService('kibanaServer'); + const transform = getService('transform'); + const svlUserManager = getService('svlUserManager'); + const svlCommonApi = getService('svlCommonApi'); + + describe('reset_slo', () => { + // DATE_VIEW should match the index template: + // x-pack/packages/kbn-infra-forge/src/data_sources/composable/template.json + const DATE_VIEW = 'kbn-data-forge-fake_hosts'; + const DATA_VIEW_ID = 'data-view-id'; + let infraDataIndex: string; + let roleAuthc: RoleCredentials; + + before(async () => { + infraDataIndex = await generate({ + esClient, + lookback: 'now-15m', + logger, + }); + await dataViewApi.create({ + name: DATE_VIEW, + id: DATA_VIEW_ID, + title: DATE_VIEW, + }); + await kibanaServer.savedObjects.cleanStandardList(); + roleAuthc = await svlUserManager.createM2mApiKeyWithRoleScope('admin'); + }); + + after(async () => { + await dataViewApi.delete({ + id: DATA_VIEW_ID, + }); + await supertest + .delete('/api/observability/slos/my-custom-id1') + .set(svlCommonApi.getInternalRequestHeader()); + + await supertest + .delete('/api/observability/slos/my-custom-id2') + .set(svlCommonApi.getInternalRequestHeader()); + + await esDeleteAllIndices([infraDataIndex]); + await cleanup({ esClient, logger }); + await kibanaServer.savedObjects.clean({ types: [SO_SLO_TYPE] }); + await transform.api.cleanTransformIndices(); + await svlUserManager.invalidateM2mApiKeyWithRoleScope(roleAuthc); + }); + + describe('with a broken SLO', () => { + const sloId = 'my-custom-id1'; + + before(async () => { + await sloApi.create( + { + id: sloId, + name: 'my custom name', + description: 'my custom description', + indicator: { + type: 'sli.kql.custom', + params: { + index: infraDataIndex, + good: 'system.cpu.total.norm.pct > 1', + total: 'system.cpu.total.norm.pct: *', + timestampField: '@timestamp', + }, + }, + timeWindow: { + duration: '7d', + type: 'rolling', + }, + budgetingMethod: 'occurrences', + objective: { + target: 0.95, + }, + groupBy: ALL_VALUE, + }, + roleAuthc + ); + + await transform.api.stopTransform(`slo-${sloId}-1`); + await transform.api.deleteTransform(`slo-${sloId}-1`); + await transform.api.stopTransform(`slo-summary-${sloId}-1`); + await transform.api.deleteTransform(`slo-summary-${sloId}-1`); + }); + + it('resets the SLO resources', async () => { + const expectedRevision = 1; + const sloResponse = await sloApi.waitForSloReseted(sloId, roleAuthc); + expect(sloResponse.revision).to.eql(expectedRevision); + + // assert resources are reinstalled + const rollupPipelineResponse = await esClient.ingest.getPipeline({ + id: getSLOPipelineId(sloId, expectedRevision), + }); + const expectedRollupPipeline = `.slo-observability.sli.pipeline-${sloId}-${expectedRevision}`; + expect(rollupPipelineResponse[expectedRollupPipeline]).not.to.be(undefined); + + const summaryPipelineResponse = await esClient.ingest.getPipeline({ + id: getSLOSummaryPipelineId(sloId, expectedRevision), + }); + const expectedSummaryPipeline = `.slo-observability.summary.pipeline-${sloId}-${expectedRevision}`; + expect(summaryPipelineResponse[expectedSummaryPipeline]).not.to.be(undefined); + + const sloTransformId = `slo-${sloId}-${expectedRevision}`; + await transform.api.getTransform(sloTransformId, 200); + const sloSummaryTransformId = `slo-summary-${sloId}-${expectedRevision}`; + await transform.api.getTransform(sloSummaryTransformId, 200); + }); + }); + }); +} diff --git a/x-pack/test_serverless/api_integration/test_suites/observability/slos/update_slo.ts b/x-pack/test_serverless/api_integration/test_suites/observability/slos/update_slo.ts new file mode 100644 index 0000000000000..ff04f1d662191 --- /dev/null +++ b/x-pack/test_serverless/api_integration/test_suites/observability/slos/update_slo.ts @@ -0,0 +1,222 @@ +/* + * 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 { cleanup, generate } from '@kbn/infra-forge'; +import { getSLOPipelineId, getSLOSummaryPipelineId } from '@kbn/slo-plugin/common/constants'; +import { SO_SLO_TYPE } from '@kbn/slo-plugin/server/saved_objects'; +import { ALL_VALUE } from '@kbn/slo-schema'; +import type { RoleCredentials } from '../../../../shared/services'; +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ getService }: FtrProviderContext) { + const esClient = getService('es'); + const supertest = getService('supertest'); + + const esDeleteAllIndices = getService('esDeleteAllIndices'); + const logger = getService('log'); + const dataViewApi = getService('dataViewApi'); + const sloApi = getService('sloApi'); + const kibanaServer = getService('kibanaServer'); + const transform = getService('transform'); + const svlUserManager = getService('svlUserManager'); + const svlCommonApi = getService('svlCommonApi'); + + describe('update_slo', () => { + // DATE_VIEW should match the index template: + // x-pack/packages/kbn-infra-forge/src/data_sources/composable/template.json + const DATE_VIEW = 'kbn-data-forge-fake_hosts'; + const DATA_VIEW_ID = 'data-view-id'; + let infraDataIndex: string; + let roleAuthc: RoleCredentials; + + before(async () => { + infraDataIndex = await generate({ + esClient, + lookback: 'now-15m', + logger, + }); + await dataViewApi.create({ + name: DATE_VIEW, + id: DATA_VIEW_ID, + title: DATE_VIEW, + }); + await kibanaServer.savedObjects.cleanStandardList(); + roleAuthc = await svlUserManager.createM2mApiKeyWithRoleScope('admin'); + }); + + after(async () => { + await dataViewApi.delete({ + id: DATA_VIEW_ID, + }); + await supertest + .delete('/api/observability/slos/my-custom-id1') + .set(svlCommonApi.getInternalRequestHeader()); + + await supertest + .delete('/api/observability/slos/my-custom-id2') + .set(svlCommonApi.getInternalRequestHeader()); + + await esDeleteAllIndices([infraDataIndex]); + await cleanup({ esClient, logger }); + await kibanaServer.savedObjects.clean({ types: [SO_SLO_TYPE] }); + await transform.api.cleanTransformIndices(); + await svlUserManager.invalidateM2mApiKeyWithRoleScope(roleAuthc); + }); + + describe('when updating fields without revision bump', () => { + const sloId = 'my-custom-id1'; + + before(async () => { + await sloApi.create( + { + id: sloId, + name: 'my custom name', + description: 'my custom description', + indicator: { + type: 'sli.kql.custom', + params: { + index: infraDataIndex, + good: 'system.cpu.total.norm.pct > 1', + total: 'system.cpu.total.norm.pct: *', + timestampField: '@timestamp', + }, + }, + timeWindow: { + duration: '7d', + type: 'rolling', + }, + budgetingMethod: 'occurrences', + objective: { + target: 0.999, + }, + groupBy: ALL_VALUE, + }, + roleAuthc + ); + }); + + it('updates the SO definition', async () => { + let sloResponse = await sloApi.waitForSloCreated({ sloId, roleAuthc }); + expect(sloResponse.name).to.eql('my custom name'); + expect(sloResponse.description).to.eql('my custom description'); + expect(sloResponse.revision).to.eql(1); + + await sloApi.update( + { + sloId, + slo: { + name: 'updated name', + description: 'updated description', + }, + }, + roleAuthc + ); + + // assert definition is updated + sloResponse = await sloApi.waitForSloCreated({ sloId, roleAuthc }); + expect(sloResponse.name).to.eql('updated name'); + expect(sloResponse.description).to.eql('updated description'); + expect(sloResponse.revision).to.eql(1); + + // assert resources are not reinstalled + const expectedRevision = 1; + const rollupPipelineResponse = await esClient.ingest.getPipeline({ + id: getSLOPipelineId(sloId, expectedRevision), + }); + const expectedRollupPipeline = `.slo-observability.sli.pipeline-${sloId}-${expectedRevision}`; + expect(rollupPipelineResponse[expectedRollupPipeline]).not.to.be(undefined); + + const summaryPipelineResponse = await esClient.ingest.getPipeline({ + id: getSLOSummaryPipelineId(sloId, expectedRevision), + }); + const expectedSummaryPipeline = `.slo-observability.summary.pipeline-${sloId}-${expectedRevision}`; + expect(summaryPipelineResponse[expectedSummaryPipeline]).not.to.be(undefined); + + const sloTransformId = `slo-${sloId}-${expectedRevision}`; + await transform.api.getTransform(sloTransformId, 200); + const sloSummaryTransformId = `slo-summary-${sloId}-${expectedRevision}`; + await transform.api.getTransform(sloSummaryTransformId, 200); + }); + }); + + describe('when updating fields with revision bump', () => { + const sloId = 'my-custom-id2'; + + before(async () => { + await sloApi.create( + { + id: sloId, + name: 'my custom name', + description: 'my custom description', + indicator: { + type: 'sli.kql.custom', + params: { + index: infraDataIndex, + good: 'system.cpu.total.norm.pct > 1', + total: 'system.cpu.total.norm.pct: *', + timestampField: '@timestamp', + }, + }, + timeWindow: { + duration: '7d', + type: 'rolling', + }, + budgetingMethod: 'occurrences', + objective: { + target: 0.95, + }, + groupBy: ALL_VALUE, + }, + roleAuthc + ); + }); + + it('updates the SO definition and reinstall the resources', async () => { + let sloResponse = await sloApi.waitForSloCreated({ sloId, roleAuthc }); + expect(sloResponse.objective).to.eql({ target: 0.95 }); + expect(sloResponse.revision).to.eql(1); + + await sloApi.update( + { + sloId, + slo: { + objective: { + target: 0.8, + }, + }, + }, + roleAuthc + ); + + // assert definition is updated + sloResponse = await sloApi.waitForSloCreated({ sloId, roleAuthc }); + expect(sloResponse.objective).to.eql({ target: 0.8 }); + expect(sloResponse.revision).to.eql(2); + + // assert resources are reinstalled + const expectedRevision = 2; + const rollupPipelineResponse = await esClient.ingest.getPipeline({ + id: getSLOPipelineId(sloId, expectedRevision), + }); + const expectedRollupPipeline = `.slo-observability.sli.pipeline-${sloId}-${expectedRevision}`; + expect(rollupPipelineResponse[expectedRollupPipeline]).not.to.be(undefined); + + const summaryPipelineResponse = await esClient.ingest.getPipeline({ + id: getSLOSummaryPipelineId(sloId, expectedRevision), + }); + const expectedSummaryPipeline = `.slo-observability.summary.pipeline-${sloId}-${expectedRevision}`; + expect(summaryPipelineResponse[expectedSummaryPipeline]).not.to.be(undefined); + + const sloTransformId = `slo-${sloId}-${expectedRevision}`; + await transform.api.getTransform(sloTransformId, 200); + const sloSummaryTransformId = `slo-summary-${sloId}-${expectedRevision}`; + await transform.api.getTransform(sloSummaryTransformId, 200); + }); + }); + }); +}