diff --git a/x-pack/packages/kbn-slo-schema/src/rest_specs/slo.ts b/x-pack/packages/kbn-slo-schema/src/rest_specs/slo.ts index 17b8e86d9fc5a..706213003250b 100644 --- a/x-pack/packages/kbn-slo-schema/src/rest_specs/slo.ts +++ b/x-pack/packages/kbn-slo-schema/src/rest_specs/slo.ts @@ -158,6 +158,10 @@ const findSLOResponseSchema = t.type({ results: t.array(sloWithSummaryResponseSchema), }); +const deleteSLOInstancesParamsSchema = t.type({ + body: t.type({ list: t.array(t.type({ sloId: sloIdSchema, instanceId: t.string })) }), +}); + const fetchHistoricalSummaryParamsSchema = t.type({ body: t.type({ list: t.array(t.type({ sloId: sloIdSchema, instanceId: allOrAnyString })) }), }); @@ -239,6 +243,9 @@ type UpdateSLOResponse = t.OutputOf; type FindSLOParams = t.TypeOf; type FindSLOResponse = t.OutputOf; +type DeleteSLOInstancesInput = t.OutputOf; +type DeleteSLOInstancesParams = t.TypeOf; + type FetchHistoricalSummaryParams = t.TypeOf; type FetchHistoricalSummaryResponse = t.OutputOf; type HistoricalSummaryResponse = t.OutputOf; @@ -269,6 +276,7 @@ type KQLCustomIndicator = t.OutputOf; export { createSLOParamsSchema, deleteSLOParamsSchema, + deleteSLOInstancesParamsSchema, findSLOParamsSchema, findSLOResponseSchema, getPreviewDataParamsSchema, @@ -294,6 +302,8 @@ export type { CreateSLOInput, CreateSLOParams, CreateSLOResponse, + DeleteSLOInstancesInput, + DeleteSLOInstancesParams, FindSLOParams, FindSLOResponse, GetPreviewDataParams, diff --git a/x-pack/plugins/observability/docs/openapi/slo/bundled.json b/x-pack/plugins/observability/docs/openapi/slo/bundled.json index 374d35c9dd212..559f5713e2c35 100644 --- a/x-pack/plugins/observability/docs/openapi/slo/bundled.json +++ b/x-pack/plugins/observability/docs/openapi/slo/bundled.json @@ -677,6 +677,74 @@ } } } + }, + "/s/{spaceId}/api/observability/slos/_delete_instances": { + "post": { + "summary": "Batch delete rollup and summary data for the matching list of sloId and instanceId", + "operationId": "deleteSloInstancesOp", + "description": "You must have `all` privileges for the **SLOs** feature in the **Observability** section of the Kibana feature privileges.\n", + "tags": [ + "slo" + ], + "parameters": [ + { + "$ref": "#/components/parameters/kbn_xsrf" + }, + { + "$ref": "#/components/parameters/space_id" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/delete_slo_instances_request" + } + } + } + }, + "responses": { + "204": { + "description": "Successful request" + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/400_response" + } + } + } + }, + "401": { + "description": "Unauthorized response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/401_response" + } + } + } + }, + "403": { + "description": "Unauthorized response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/403_response" + } + } + } + } + }, + "servers": [ + { + "url": "https://localhost:5601" + } + ] + } } }, "components": { @@ -1718,10 +1786,10 @@ "title": "Historical summary request", "type": "object", "required": [ - "sloIds" + "list" ], "properties": { - "sloIds": { + "list": { "description": "The list of SLO identifiers to get the historical summary for", "type": "array", "items": { @@ -1756,6 +1824,39 @@ } } } + }, + "delete_slo_instances_request": { + "title": "Delete SLO instances request", + "description": "The delete SLO instances request takes a list of SLO id and instance id, then delete the rollup and summary data. This API can be used to remove the staled data of an instance SLO that no longer get updated.\n", + "type": "object", + "required": [ + "list" + ], + "properties": { + "list": { + "description": "An array of slo id and instance id", + "type": "array", + "items": { + "type": "object", + "required": [ + "sloId", + "instanceId" + ], + "properties": { + "sloId": { + "description": "The SLO unique identifier", + "type": "string", + "example": "8853df00-ae2e-11ed-90af-09bb6422b258" + }, + "instanceId": { + "description": "The SLO instance identifier", + "type": "string", + "example": "8853df00-ae2e-11ed-90af-09bb6422b258" + } + } + } + } + } } } } diff --git a/x-pack/plugins/observability/docs/openapi/slo/bundled.yaml b/x-pack/plugins/observability/docs/openapi/slo/bundled.yaml index a6cdf5c376485..efeeb090f0156 100644 --- a/x-pack/plugins/observability/docs/openapi/slo/bundled.yaml +++ b/x-pack/plugins/observability/docs/openapi/slo/bundled.yaml @@ -408,6 +408,46 @@ paths: application/json: schema: $ref: '#/components/schemas/403_response' + /s/{spaceId}/api/observability/slos/_delete_instances: + post: + summary: Batch delete rollup and summary data for the matching list of sloId and instanceId + operationId: deleteSloInstancesOp + description: | + You must have `all` privileges for the **SLOs** feature in the **Observability** section of the Kibana feature privileges. + tags: + - slo + parameters: + - $ref: '#/components/parameters/kbn_xsrf' + - $ref: '#/components/parameters/space_id' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/delete_slo_instances_request' + responses: + '204': + description: Successful request + '400': + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/400_response' + '401': + description: Unauthorized response + content: + application/json: + schema: + $ref: '#/components/schemas/401_response' + '403': + description: Unauthorized response + content: + application/json: + schema: + $ref: '#/components/schemas/403_response' + servers: + - url: https://localhost:5601 components: securitySchemes: basicAuth: @@ -1190,9 +1230,9 @@ components: title: Historical summary request type: object required: - - sloIds + - list properties: - sloIds: + list: description: The list of SLO identifiers to get the historical summary for type: array items: @@ -1216,3 +1256,28 @@ components: example: 0.9836 errorBudget: $ref: '#/components/schemas/error_budget' + delete_slo_instances_request: + title: Delete SLO instances request + description: | + The delete SLO instances request takes a list of SLO id and instance id, then delete the rollup and summary data. This API can be used to remove the staled data of an instance SLO that no longer get updated. + type: object + required: + - list + properties: + list: + description: An array of slo id and instance id + type: array + items: + type: object + required: + - sloId + - instanceId + properties: + sloId: + description: The SLO unique identifier + type: string + example: 8853df00-ae2e-11ed-90af-09bb6422b258 + instanceId: + description: The SLO instance identifier + type: string + example: 8853df00-ae2e-11ed-90af-09bb6422b258 diff --git a/x-pack/plugins/observability/docs/openapi/slo/components/schemas/delete_slo_instances_request.yaml b/x-pack/plugins/observability/docs/openapi/slo/components/schemas/delete_slo_instances_request.yaml new file mode 100644 index 0000000000000..819050a915df5 --- /dev/null +++ b/x-pack/plugins/observability/docs/openapi/slo/components/schemas/delete_slo_instances_request.yaml @@ -0,0 +1,26 @@ +title: Delete SLO instances request +description: > + The delete SLO instances request takes a list of SLO id and instance id, then delete the rollup and summary data. + This API can be used to remove the staled data of an instance SLO that no longer get updated. +type: object +required: + - list +properties: + list: + description: An array of slo id and instance id + type: array + items: + type: object + required: + - sloId + - instanceId + properties: + sloId: + description: The SLO unique identifier + type: string + example: 8853df00-ae2e-11ed-90af-09bb6422b258 + instanceId: + description: The SLO instance identifier + type: string + example: 8853df00-ae2e-11ed-90af-09bb6422b258 + \ No newline at end of file diff --git a/x-pack/plugins/observability/docs/openapi/slo/components/schemas/historical_summary_request.yaml b/x-pack/plugins/observability/docs/openapi/slo/components/schemas/historical_summary_request.yaml index 737a5b83f03f9..a2be13fc9842d 100644 --- a/x-pack/plugins/observability/docs/openapi/slo/components/schemas/historical_summary_request.yaml +++ b/x-pack/plugins/observability/docs/openapi/slo/components/schemas/historical_summary_request.yaml @@ -1,9 +1,9 @@ title: Historical summary request type: object required: - - sloIds + - list properties: - sloIds: + list: description: The list of SLO identifiers to get the historical summary for type: array items: diff --git a/x-pack/plugins/observability/docs/openapi/slo/entrypoint.yaml b/x-pack/plugins/observability/docs/openapi/slo/entrypoint.yaml index 44f16ed4585e0..ee722573efa91 100644 --- a/x-pack/plugins/observability/docs/openapi/slo/entrypoint.yaml +++ b/x-pack/plugins/observability/docs/openapi/slo/entrypoint.yaml @@ -31,6 +31,8 @@ paths: $ref: "paths/s@{spaceid}@api@slos@{sloid}@{disable}.yaml" "/s/{spaceId}/internal/observability/slos/_historical_summary": $ref: "paths/s@{spaceid}@api@slos@_historical_summary.yaml" + "/s/{spaceId}/api/observability/slos/_delete_instances": + $ref: "paths/s@{spaceid}@api@slos@_delete_instances.yaml" components: securitySchemes: basicAuth: diff --git a/x-pack/plugins/observability/docs/openapi/slo/paths/s@{spaceid}@api@slos@_delete_instances.yaml b/x-pack/plugins/observability/docs/openapi/slo/paths/s@{spaceid}@api@slos@_delete_instances.yaml new file mode 100644 index 0000000000000..e9775576695a2 --- /dev/null +++ b/x-pack/plugins/observability/docs/openapi/slo/paths/s@{spaceid}@api@slos@_delete_instances.yaml @@ -0,0 +1,40 @@ +post: + summary: Batch delete rollup and summary data for the matching list of sloId and instanceId + operationId: deleteSloInstancesOp + description: > + You must have `all` privileges for the **SLOs** feature in the + **Observability** section of the Kibana feature privileges. + tags: + - slo + parameters: + - $ref: ../components/headers/kbn_xsrf.yaml + - $ref: ../components/parameters/space_id.yaml + requestBody: + required: true + content: + application/json: + schema: + $ref: '../components/schemas/delete_slo_instances_request.yaml' + responses: + '204': + description: Successful request + '400': + description: Bad request + content: + application/json: + schema: + $ref: '../components/schemas/400_response.yaml' + '401': + description: Unauthorized response + content: + application/json: + schema: + $ref: '../components/schemas/401_response.yaml' + '403': + description: Unauthorized response + content: + application/json: + schema: + $ref: '../components/schemas/403_response.yaml' + servers: + - url: https://localhost:5601 diff --git a/x-pack/plugins/observability/server/routes/slo/route.ts b/x-pack/plugins/observability/server/routes/slo/route.ts index f77f173675c68..99eec0b54ee5a 100644 --- a/x-pack/plugins/observability/server/routes/slo/route.ts +++ b/x-pack/plugins/observability/server/routes/slo/route.ts @@ -9,6 +9,7 @@ import { errors } from '@elastic/elasticsearch'; import { failedDependency, forbidden } from '@hapi/boom'; import { createSLOParamsSchema, + deleteSLOInstancesParamsSchema, deleteSLOParamsSchema, fetchHistoricalSummaryParamsSchema, findSloDefinitionsParamsSchema, @@ -26,6 +27,7 @@ import { DefaultSummaryClient, DefaultTransformManager, DeleteSLO, + DeleteSLOInstances, FindSLO, GetSLO, KibanaSavedObjectsSLORepository, @@ -225,6 +227,22 @@ const findSLORoute = createObservabilityServerRoute({ }, }); +const deleteSloInstancesRoute = createObservabilityServerRoute({ + endpoint: 'POST /api/observability/slos/_delete_instances 2023-10-31', + options: { + tags: ['access:slo_write'], + }, + params: deleteSLOInstancesParamsSchema, + handler: async ({ context, params }) => { + await assertPlatinumLicense(context); + + const esClient = (await context.core).elasticsearch.client.asCurrentUser; + const deleteSloInstances = new DeleteSLOInstances(esClient); + + await deleteSloInstances.execute(params.body); + }, +}); + const findSloDefinitionsRoute = createObservabilityServerRoute({ endpoint: 'GET /internal/observability/slos/_definitions', options: { @@ -351,6 +369,7 @@ const getPreviewData = createObservabilityServerRoute({ export const sloRouteRepository = { ...createSLORoute, ...deleteSLORoute, + ...deleteSloInstancesRoute, ...disableSLORoute, ...enableSLORoute, ...fetchHistoricalSummary, diff --git a/x-pack/plugins/observability/server/services/slo/delete_slo_instances.test.ts b/x-pack/plugins/observability/server/services/slo/delete_slo_instances.test.ts new file mode 100644 index 0000000000000..bd3826e31fa92 --- /dev/null +++ b/x-pack/plugins/observability/server/services/slo/delete_slo_instances.test.ts @@ -0,0 +1,165 @@ +/* + * 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 { ElasticsearchClient } from '@kbn/core/server'; +import { elasticsearchServiceMock } from '@kbn/core/server/mocks'; +import { DeleteSLOInstances } from './delete_slo_instances'; + +describe('DeleteSLOInstances', () => { + let mockEsClient: jest.Mocked; + let deleteSLOInstances: DeleteSLOInstances; + + beforeEach(() => { + mockEsClient = elasticsearchServiceMock.createElasticsearchClient(); + deleteSLOInstances = new DeleteSLOInstances(mockEsClient); + }); + + describe('validation', () => { + it("forbids deleting an SLO with an '*' (all) instance id", async () => { + await expect( + deleteSLOInstances.execute({ + list: [ + { sloId: 'first', instanceId: 'irrelevant' }, + { sloId: 'second', instanceId: '*' }, + ], + }) + ).rejects.toThrowError("Cannot delete an SLO instance '*'"); + }); + }); + + it('deletes the roll up and the summary data for each tuple', async () => { + await deleteSLOInstances.execute({ + list: [ + { sloId: 'first', instanceId: 'host-foo' }, + { sloId: 'second', instanceId: 'host-foo' }, + { sloId: 'third', instanceId: 'cluster-eu' }, + ], + }); + + expect(mockEsClient.deleteByQuery).toHaveBeenCalledTimes(2); + expect(mockEsClient.deleteByQuery.mock.calls[0][0]).toMatchInlineSnapshot(` + Object { + "index": ".slo-observability.sli-v2*", + "query": Object { + "bool": Object { + "should": Array [ + Object { + "bool": Object { + "must": Array [ + Object { + "term": Object { + "slo.id": "first", + }, + }, + Object { + "term": Object { + "slo.instanceId": "host-foo", + }, + }, + ], + }, + }, + Object { + "bool": Object { + "must": Array [ + Object { + "term": Object { + "slo.id": "second", + }, + }, + Object { + "term": Object { + "slo.instanceId": "host-foo", + }, + }, + ], + }, + }, + Object { + "bool": Object { + "must": Array [ + Object { + "term": Object { + "slo.id": "third", + }, + }, + Object { + "term": Object { + "slo.instanceId": "cluster-eu", + }, + }, + ], + }, + }, + ], + }, + }, + "wait_for_completion": false, + } + `); + expect(mockEsClient.deleteByQuery.mock.calls[1][0]).toMatchInlineSnapshot(` + Object { + "index": ".slo-observability.summary-v2*", + "query": Object { + "bool": Object { + "should": Array [ + Object { + "bool": Object { + "must": Array [ + Object { + "term": Object { + "slo.id": "first", + }, + }, + Object { + "term": Object { + "slo.instanceId": "host-foo", + }, + }, + ], + }, + }, + Object { + "bool": Object { + "must": Array [ + Object { + "term": Object { + "slo.id": "second", + }, + }, + Object { + "term": Object { + "slo.instanceId": "host-foo", + }, + }, + ], + }, + }, + Object { + "bool": Object { + "must": Array [ + Object { + "term": Object { + "slo.id": "third", + }, + }, + Object { + "term": Object { + "slo.instanceId": "cluster-eu", + }, + }, + ], + }, + }, + ], + }, + }, + "wait_for_completion": false, + } + `); + }); +}); diff --git a/x-pack/plugins/observability/server/services/slo/delete_slo_instances.ts b/x-pack/plugins/observability/server/services/slo/delete_slo_instances.ts new file mode 100644 index 0000000000000..f1892122622d6 --- /dev/null +++ b/x-pack/plugins/observability/server/services/slo/delete_slo_instances.ts @@ -0,0 +1,71 @@ +/* + * 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 { ElasticsearchClient } from '@kbn/core/server'; +import { ALL_VALUE, DeleteSLOInstancesParams } from '@kbn/slo-schema'; +import { + SLO_DESTINATION_INDEX_PATTERN, + SLO_SUMMARY_DESTINATION_INDEX_PATTERN, +} from '../../assets/constants'; +import { IllegalArgumentError } from '../../errors'; + +interface SloInstanceTuple { + sloId: string; + instanceId: string; +} + +export class DeleteSLOInstances { + constructor(private esClient: ElasticsearchClient) {} + + public async execute(params: DeleteSLOInstancesParams): Promise { + const containsAllValueInstanceId = params.list.some((item) => item.instanceId === ALL_VALUE); + if (containsAllValueInstanceId) { + throw new IllegalArgumentError("Cannot delete an SLO instance '*'"); + } + + await this.deleteRollupData(params.list); + await this.deleteSummaryData(params.list); + } + + private async deleteRollupData(list: SloInstanceTuple[]): Promise { + await this.esClient.deleteByQuery({ + index: SLO_DESTINATION_INDEX_PATTERN, + wait_for_completion: false, + query: { + bool: { + should: list.map((item) => ({ + bool: { + must: [ + { term: { 'slo.id': item.sloId } }, + { term: { 'slo.instanceId': item.instanceId } }, + ], + }, + })), + }, + }, + }); + } + + private async deleteSummaryData(list: SloInstanceTuple[]): Promise { + await this.esClient.deleteByQuery({ + index: SLO_SUMMARY_DESTINATION_INDEX_PATTERN, + wait_for_completion: false, + query: { + bool: { + should: list.map((item) => ({ + bool: { + must: [ + { term: { 'slo.id': item.sloId } }, + { term: { 'slo.instanceId': item.instanceId } }, + ], + }, + })), + }, + }, + }); + } +} diff --git a/x-pack/plugins/observability/server/services/slo/index.ts b/x-pack/plugins/observability/server/services/slo/index.ts index 396b443be7eeb..7c99c289ae90b 100644 --- a/x-pack/plugins/observability/server/services/slo/index.ts +++ b/x-pack/plugins/observability/server/services/slo/index.ts @@ -7,6 +7,7 @@ export * from './create_slo'; export * from './delete_slo'; +export * from './delete_slo_instances'; export * from './fetch_historical_summary'; export * from './find_slo'; export * from './get_slo';