diff --git a/oas_docs/output/kibana.serverless.yaml b/oas_docs/output/kibana.serverless.yaml index 6be8b706afc59..3467aac9ba8e4 100644 --- a/oas_docs/output/kibana.serverless.yaml +++ b/oas_docs/output/kibana.serverless.yaml @@ -45897,6 +45897,91 @@ paths: summary: Create an SLO tags: - slo + /s/{spaceId}/api/observability/slos/_bulk_delete: + post: + description: | + Bulk delete SLO definitions and their associated summary and rollup data. This endpoint initiates a bulk deletion operation for SLOs, which may take some time to complete. The status of the operation can be checked using the `GET /api/slo/_bulk_delete/{taskId}` endpoint. + operationId: bulkDeleteOp + parameters: + - $ref: '#/components/parameters/SLOs_kbn_xsrf' + - $ref: '#/components/parameters/SLOs_space_id' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/SLOs_bulk_delete_request' + required: true + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/SLOs_bulk_delete_response' + description: Successful response + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/SLOs_400_response' + description: Bad request + '401': + content: + application/json: + schema: + $ref: '#/components/schemas/SLOs_401_response' + description: Unauthorized response + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/SLOs_403_response' + description: Unauthorized response + summary: Bulk delete SLO definitions and their associated summary and rollup data. + tags: + - slo + /s/{spaceId}/api/observability/slos/_bulk_delete/{taskId}: + get: + description: | + Retrieve the status of the bulk deletion operation for SLOs. This endpoint returns the status of the bulk deletion operation, including whether it is completed and the results of the operation. + operationId: bulkDeleteStatusOp + parameters: + - $ref: '#/components/parameters/SLOs_kbn_xsrf' + - $ref: '#/components/parameters/SLOs_space_id' + - description: The task id of the bulk delete operation + in: path + name: taskId + required: true + schema: + example: 8853df00-ae2e-11ed-90af-09bb6422b258 + type: string + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/SLOs_bulk_delete_status_response' + description: Successful response + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/SLOs_400_response' + description: Bad request + '401': + content: + application/json: + schema: + $ref: '#/components/schemas/SLOs_401_response' + description: Unauthorized response + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/SLOs_403_response' + description: Unauthorized response + summary: Retrieve the status of the bulk deletion + tags: + - slo /s/{spaceId}/api/observability/slos/_bulk_purge_rollup: post: description: | @@ -62085,6 +62170,62 @@ components: example: occurrences title: Budgeting method type: string + SLOs_bulk_delete_request: + description: | + The bulk delete SLO request takes a list of SLOs Definition id to delete. + properties: + list: + description: An array of SLO Definition id + items: + description: The SLO Definition id + example: 8853df00-ae2e-11ed-90af-09bb6422b258 + type: string + type: array + required: + - list + title: Bulk delete SLO request + type: object + SLOs_bulk_delete_response: + description: | + The bulk delete SLO response returns a taskId that can be used to poll for its status + properties: + taskId: + description: The taskId of the bulk delete operation + example: d08506b7-f0e8-4f8b-a06a-a83940f4db91 + type: string + title: Bulk delete SLO response + type: object + SLOs_bulk_delete_status_response: + description: Indicates if the bulk deletion is completed, with the detailed results of the operation. + properties: + error: + description: The error message if the bulk deletion operation failed + example: Task not found + type: string + isDone: + description: Indicates if the bulk deletion operation is completed + example: true + type: boolean + results: + description: The results of the bulk deletion operation, including the success status and any errors for each SLO + items: + type: object + properties: + error: + description: The error message if the deletion operation failed for this SLO + example: SLO [d08506b7-f0e8-4f8b-a06a-a83940f4db91] not found + type: string + id: + description: The ID of the SLO that was deleted + example: d08506b7-f0e8-4f8b-a06a-a83940f4db91 + type: string + success: + description: The result of the deletion operation for this SLO + example: true + type: boolean + type: array + title: The status of the bulk deletion + type: object SLOs_bulk_purge_rollup_request: description: | The bulk purge rollup data request takes a list of SLO ids and a purge policy, then deletes the rollup data according to the purge policy. This API can be used to remove the staled data of an instance SLO that no longer get updated. diff --git a/oas_docs/output/kibana.yaml b/oas_docs/output/kibana.yaml index 31e94d787a20d..1f8cceefb5bf5 100644 --- a/oas_docs/output/kibana.yaml +++ b/oas_docs/output/kibana.yaml @@ -24918,6 +24918,91 @@ paths: summary: Create an SLO tags: - slo + /s/{spaceId}/api/observability/slos/_bulk_delete: + post: + description: | + Bulk delete SLO definitions and their associated summary and rollup data. This endpoint initiates a bulk deletion operation for SLOs, which may take some time to complete. The status of the operation can be checked using the `GET /api/slo/_bulk_delete/{taskId}` endpoint. + operationId: bulkDeleteOp + parameters: + - $ref: '#/components/parameters/SLOs_kbn_xsrf' + - $ref: '#/components/parameters/SLOs_space_id' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/SLOs_bulk_delete_request' + required: true + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/SLOs_bulk_delete_response' + description: Successful response + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/SLOs_400_response' + description: Bad request + '401': + content: + application/json: + schema: + $ref: '#/components/schemas/SLOs_401_response' + description: Unauthorized response + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/SLOs_403_response' + description: Unauthorized response + summary: Bulk delete SLO definitions and their associated summary and rollup data. + tags: + - slo + /s/{spaceId}/api/observability/slos/_bulk_delete/{taskId}: + get: + description: | + Retrieve the status of the bulk deletion operation for SLOs. This endpoint returns the status of the bulk deletion operation, including whether it is completed and the results of the operation. + operationId: bulkDeleteStatusOp + parameters: + - $ref: '#/components/parameters/SLOs_kbn_xsrf' + - $ref: '#/components/parameters/SLOs_space_id' + - description: The task id of the bulk delete operation + in: path + name: taskId + required: true + schema: + example: 8853df00-ae2e-11ed-90af-09bb6422b258 + type: string + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/SLOs_bulk_delete_status_response' + description: Successful response + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/SLOs_400_response' + description: Bad request + '401': + content: + application/json: + schema: + $ref: '#/components/schemas/SLOs_401_response' + description: Unauthorized response + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/SLOs_403_response' + description: Unauthorized response + summary: Retrieve the status of the bulk deletion + tags: + - slo /s/{spaceId}/api/observability/slos/_bulk_purge_rollup: post: description: | @@ -46127,6 +46212,62 @@ components: example: occurrences title: Budgeting method type: string + SLOs_bulk_delete_request: + description: | + The bulk delete SLO request takes a list of SLOs Definition id to delete. + properties: + list: + description: An array of SLO Definition id + items: + description: The SLO Definition id + example: 8853df00-ae2e-11ed-90af-09bb6422b258 + type: string + type: array + required: + - list + title: Bulk delete SLO request + type: object + SLOs_bulk_delete_response: + description: | + The bulk delete SLO response returns a taskId that can be used to poll for its status + properties: + taskId: + description: The taskId of the bulk delete operation + example: d08506b7-f0e8-4f8b-a06a-a83940f4db91 + type: string + title: Bulk delete SLO response + type: object + SLOs_bulk_delete_status_response: + description: Indicates if the bulk deletion is completed, with the detailed results of the operation. + properties: + error: + description: The error message if the bulk deletion operation failed + example: Task not found + type: string + isDone: + description: Indicates if the bulk deletion operation is completed + example: true + type: boolean + results: + description: The results of the bulk deletion operation, including the success status and any errors for each SLO + items: + type: object + properties: + error: + description: The error message if the deletion operation failed for this SLO + example: SLO [d08506b7-f0e8-4f8b-a06a-a83940f4db91] not found + type: string + id: + description: The ID of the SLO that was deleted + example: d08506b7-f0e8-4f8b-a06a-a83940f4db91 + type: string + success: + description: The result of the deletion operation for this SLO + example: true + type: boolean + type: array + title: The status of the bulk deletion + type: object SLOs_bulk_purge_rollup_request: description: | The bulk purge rollup data request takes a list of SLO ids and a purge policy, then deletes the rollup data according to the purge policy. This API can be used to remove the staled data of an instance SLO that no longer get updated. diff --git a/x-pack/platform/packages/shared/kbn-slo-schema/src/rest_specs/routes/bulk_delete.ts b/x-pack/platform/packages/shared/kbn-slo-schema/src/rest_specs/routes/bulk_delete.ts new file mode 100644 index 0000000000000..546100b12bdf3 --- /dev/null +++ b/x-pack/platform/packages/shared/kbn-slo-schema/src/rest_specs/routes/bulk_delete.ts @@ -0,0 +1,38 @@ +/* + * 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 * as t from 'io-ts'; +import { sloIdSchema } from '../../schema/slo'; + +const bulkDeleteParamsSchema = t.type({ + body: t.type({ + list: t.array(sloIdSchema), + }), +}); + +const bulkDeleteStatusParamsSchema = t.type({ + path: t.type({ + taskId: t.string, + }), +}); + +type BulkDeleteInput = t.OutputOf; +type BulkDeleteParams = t.TypeOf; + +interface BulkDeleteResult { + id: string; + success: boolean; + error?: string; +} + +interface BulkDeleteStatusResponse { + isDone: boolean; + results?: BulkDeleteResult[]; + error?: string; +} + +export type { BulkDeleteInput, BulkDeleteParams, BulkDeleteResult, BulkDeleteStatusResponse }; +export { bulkDeleteParamsSchema, bulkDeleteStatusParamsSchema }; diff --git a/x-pack/platform/packages/shared/kbn-slo-schema/src/rest_specs/routes/index.ts b/x-pack/platform/packages/shared/kbn-slo-schema/src/rest_specs/routes/index.ts index 408d11fb2ed10..a1ac3876d3b29 100644 --- a/x-pack/platform/packages/shared/kbn-slo-schema/src/rest_specs/routes/index.ts +++ b/x-pack/platform/packages/shared/kbn-slo-schema/src/rest_specs/routes/index.ts @@ -24,3 +24,4 @@ export * from './fetch_historical_summary'; export * from './put_settings'; export * from './get_suggestions'; export * from './get_slo_health'; +export * from './bulk_delete'; diff --git a/x-pack/solutions/observability/plugins/slo/docs/openapi/slo/bundled.json b/x-pack/solutions/observability/plugins/slo/docs/openapi/slo/bundled.json index d2a787a84493e..613d80989e23f 100644 --- a/x-pack/solutions/observability/plugins/slo/docs/openapi/slo/bundled.json +++ b/x-pack/solutions/observability/plugins/slo/docs/openapi/slo/bundled.json @@ -936,6 +936,146 @@ } } } + }, + "/s/{spaceId}/api/observability/slos/_bulk_delete": { + "post": { + "summary": "Bulk delete SLO definitions and their associated summary and rollup data.", + "operationId": "bulkDeleteOp", + "description": "Bulk delete SLO definitions and their associated summary and rollup data. This endpoint initiates a bulk deletion operation for SLOs, which may take some time to complete. The status of the operation can be checked using the `GET /api/slo/_bulk_delete/{taskId}` endpoint.\n", + "tags": [ + "slo" + ], + "parameters": [ + { + "$ref": "#/components/parameters/kbn_xsrf" + }, + { + "$ref": "#/components/parameters/space_id" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/bulk_delete_request" + } + } + } + }, + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/bulk_delete_response" + } + } + } + }, + "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" + } + } + } + } + } + } + }, + "/s/{spaceId}/api/observability/slos/_bulk_delete/{taskId}": { + "get": { + "summary": "Retrieve the status of the bulk deletion", + "operationId": "bulkDeleteStatusOp", + "description": "Retrieve the status of the bulk deletion operation for SLOs. This endpoint returns the status of the bulk deletion operation, including whether it is completed and the results of the operation.\n", + "tags": [ + "slo" + ], + "parameters": [ + { + "$ref": "#/components/parameters/kbn_xsrf" + }, + { + "$ref": "#/components/parameters/space_id" + }, + { + "name": "taskId", + "in": "path", + "description": "The task id of the bulk delete operation", + "required": true, + "schema": { + "type": "string", + "example": "8853df00-ae2e-11ed-90af-09bb6422b258" + } + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/bulk_delete_status_response" + } + } + } + }, + "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" + } + } + } + } + } + } } }, "components": { @@ -2671,6 +2811,78 @@ } } } + }, + "bulk_delete_request": { + "title": "Bulk delete SLO request", + "description": "The bulk delete SLO request takes a list of SLOs Definition id to delete.\n", + "type": "object", + "required": [ + "list" + ], + "properties": { + "list": { + "description": "An array of SLO Definition id", + "type": "array", + "items": { + "type": "string", + "example": "8853df00-ae2e-11ed-90af-09bb6422b258", + "description": "The SLO Definition id" + } + } + } + }, + "bulk_delete_response": { + "title": "Bulk delete SLO response", + "description": "The bulk delete SLO response returns a taskId that can be used to poll for its status\n", + "type": "object", + "properties": { + "taskId": { + "type": "string", + "example": "d08506b7-f0e8-4f8b-a06a-a83940f4db91", + "description": "The taskId of the bulk delete operation" + } + } + }, + "bulk_delete_status_response": { + "title": "The status of the bulk deletion", + "description": "Indicates if the bulk deletion is completed, with the detailed results of the operation.", + "type": "object", + "properties": { + "isDone": { + "type": "boolean", + "example": true, + "description": "Indicates if the bulk deletion operation is completed" + }, + "error": { + "type": "string", + "example": "Task not found", + "description": "The error message if the bulk deletion operation failed" + }, + "results": { + "description": "The results of the bulk deletion operation, including the success status and any errors for each SLO", + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "example": "d08506b7-f0e8-4f8b-a06a-a83940f4db91", + "description": "The ID of the SLO that was deleted" + }, + "success": { + "type": "boolean", + "example": true, + "description": "The result of the deletion operation for this SLO" + }, + "error": { + "type": "string", + "example": "SLO [d08506b7-f0e8-4f8b-a06a-a83940f4db91] not found", + "description": "The error message if the deletion operation failed for this SLO" + } + } + } + } + } } } } diff --git a/x-pack/solutions/observability/plugins/slo/docs/openapi/slo/bundled.yaml b/x-pack/solutions/observability/plugins/slo/docs/openapi/slo/bundled.yaml index bd7f810c8c70e..b87af8a2812ca 100644 --- a/x-pack/solutions/observability/plugins/slo/docs/openapi/slo/bundled.yaml +++ b/x-pack/solutions/observability/plugins/slo/docs/openapi/slo/bundled.yaml @@ -571,6 +571,91 @@ paths: application/json: schema: $ref: '#/components/schemas/403_response' + /s/{spaceId}/api/observability/slos/_bulk_delete: + post: + summary: Bulk delete SLO definitions and their associated summary and rollup data. + operationId: bulkDeleteOp + description: | + Bulk delete SLO definitions and their associated summary and rollup data. This endpoint initiates a bulk deletion operation for SLOs, which may take some time to complete. The status of the operation can be checked using the `GET /api/slo/_bulk_delete/{taskId}` endpoint. + tags: + - slo + parameters: + - $ref: '#/components/parameters/kbn_xsrf' + - $ref: '#/components/parameters/space_id' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/bulk_delete_request' + responses: + '200': + description: Successful response + content: + application/json: + schema: + $ref: '#/components/schemas/bulk_delete_response' + '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' + /s/{spaceId}/api/observability/slos/_bulk_delete/{taskId}: + get: + summary: Retrieve the status of the bulk deletion + operationId: bulkDeleteStatusOp + description: | + Retrieve the status of the bulk deletion operation for SLOs. This endpoint returns the status of the bulk deletion operation, including whether it is completed and the results of the operation. + tags: + - slo + parameters: + - $ref: '#/components/parameters/kbn_xsrf' + - $ref: '#/components/parameters/space_id' + - name: taskId + in: path + description: The task id of the bulk delete operation + required: true + schema: + type: string + example: 8853df00-ae2e-11ed-90af-09bb6422b258 + responses: + '200': + description: Successful response + content: + application/json: + schema: + $ref: '#/components/schemas/bulk_delete_status_response' + '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' components: parameters: kbn_xsrf: @@ -1844,3 +1929,59 @@ components: description: The SLO instance identifier type: string example: 8853df00-ae2e-11ed-90af-09bb6422b258 + bulk_delete_request: + title: Bulk delete SLO request + description: | + The bulk delete SLO request takes a list of SLOs Definition id to delete. + type: object + required: + - list + properties: + list: + description: An array of SLO Definition id + type: array + items: + type: string + example: 8853df00-ae2e-11ed-90af-09bb6422b258 + description: The SLO Definition id + bulk_delete_response: + title: Bulk delete SLO response + description: | + The bulk delete SLO response returns a taskId that can be used to poll for its status + type: object + properties: + taskId: + type: string + example: d08506b7-f0e8-4f8b-a06a-a83940f4db91 + description: The taskId of the bulk delete operation + bulk_delete_status_response: + title: The status of the bulk deletion + description: Indicates if the bulk deletion is completed, with the detailed results of the operation. + type: object + properties: + isDone: + type: boolean + example: true + description: Indicates if the bulk deletion operation is completed + error: + type: string + example: Task not found + description: The error message if the bulk deletion operation failed + results: + description: The results of the bulk deletion operation, including the success status and any errors for each SLO + type: array + items: + type: object + properties: + id: + type: string + example: d08506b7-f0e8-4f8b-a06a-a83940f4db91 + description: The ID of the SLO that was deleted + success: + type: boolean + example: true + description: The result of the deletion operation for this SLO + error: + type: string + example: SLO [d08506b7-f0e8-4f8b-a06a-a83940f4db91] not found + description: The error message if the deletion operation failed for this SLO diff --git a/x-pack/solutions/observability/plugins/slo/docs/openapi/slo/components/schemas/bulk_delete_request.yaml b/x-pack/solutions/observability/plugins/slo/docs/openapi/slo/components/schemas/bulk_delete_request.yaml new file mode 100644 index 0000000000000..a8db44e078cd0 --- /dev/null +++ b/x-pack/solutions/observability/plugins/slo/docs/openapi/slo/components/schemas/bulk_delete_request.yaml @@ -0,0 +1,14 @@ +title: Bulk delete SLO request +description: > + The bulk delete SLO request takes a list of SLOs Definition id to delete. +type: object +required: + - list +properties: + list: + description: An array of SLO Definition id + type: array + items: + type: string + example: 8853df00-ae2e-11ed-90af-09bb6422b258 + description: The SLO Definition id diff --git a/x-pack/solutions/observability/plugins/slo/docs/openapi/slo/components/schemas/bulk_delete_response.yaml b/x-pack/solutions/observability/plugins/slo/docs/openapi/slo/components/schemas/bulk_delete_response.yaml new file mode 100644 index 0000000000000..408d1b030c00c --- /dev/null +++ b/x-pack/solutions/observability/plugins/slo/docs/openapi/slo/components/schemas/bulk_delete_response.yaml @@ -0,0 +1,9 @@ +title: Bulk delete SLO response +description: > + The bulk delete SLO response returns a taskId that can be used to poll for its status +type: object +properties: + taskId: + type: string + example: d08506b7-f0e8-4f8b-a06a-a83940f4db91 + description: The taskId of the bulk delete operation diff --git a/x-pack/solutions/observability/plugins/slo/docs/openapi/slo/components/schemas/bulk_delete_status_response.yaml b/x-pack/solutions/observability/plugins/slo/docs/openapi/slo/components/schemas/bulk_delete_status_response.yaml new file mode 100644 index 0000000000000..b1c6bd84b9f06 --- /dev/null +++ b/x-pack/solutions/observability/plugins/slo/docs/openapi/slo/components/schemas/bulk_delete_status_response.yaml @@ -0,0 +1,30 @@ +title: The status of the bulk deletion +description: Indicates if the bulk deletion is completed, with the detailed results of the operation. +type: object +properties: + isDone: + type: boolean + example: true + description: Indicates if the bulk deletion operation is completed + error: + type: string + example: 'Task not found' + description: The error message if the bulk deletion operation failed + results: + description: The results of the bulk deletion operation, including the success status and any errors for each SLO + type: array + items: + type: object + properties: + id: + type: string + example: d08506b7-f0e8-4f8b-a06a-a83940f4db91 + description: The ID of the SLO that was deleted + success: + type: boolean + example: true + description: The result of the deletion operation for this SLO + error: + type: string + example: SLO [d08506b7-f0e8-4f8b-a06a-a83940f4db91] not found + description: The error message if the deletion operation failed for this SLO diff --git a/x-pack/solutions/observability/plugins/slo/docs/openapi/slo/entrypoint.yaml b/x-pack/solutions/observability/plugins/slo/docs/openapi/slo/entrypoint.yaml index 71dabe8e8d24e..f732e6536f262 100644 --- a/x-pack/solutions/observability/plugins/slo/docs/openapi/slo/entrypoint.yaml +++ b/x-pack/solutions/observability/plugins/slo/docs/openapi/slo/entrypoint.yaml @@ -35,3 +35,7 @@ paths: $ref: 'paths/s@{spaceid}@api@slos@_definitions.yaml' '/s/{spaceId}/api/observability/slos/_delete_instances': $ref: 'paths/s@{spaceid}@api@slos@_delete_instances.yaml' + '/s/{spaceId}/api/observability/slos/_bulk_delete': + $ref: 'paths/s@{spaceid}@api@slos@_bulk_delete.yaml' + '/s/{spaceId}/api/observability/slos/_bulk_delete/{taskId}': + $ref: 'paths/s@{spaceid}@api@slos@_bulk_delete@{taskid}.yaml' diff --git a/x-pack/solutions/observability/plugins/slo/docs/openapi/slo/paths/s@{spaceid}@api@slos@_bulk_delete.yaml b/x-pack/solutions/observability/plugins/slo/docs/openapi/slo/paths/s@{spaceid}@api@slos@_bulk_delete.yaml new file mode 100644 index 0000000000000..79538eb6856d7 --- /dev/null +++ b/x-pack/solutions/observability/plugins/slo/docs/openapi/slo/paths/s@{spaceid}@api@slos@_bulk_delete.yaml @@ -0,0 +1,43 @@ +post: + summary: Bulk delete SLO definitions and their associated summary and rollup data. + operationId: bulkDeleteOp + description: > + Bulk delete SLO definitions and their associated summary and rollup data. + This endpoint initiates a bulk deletion operation for SLOs, which may take some time to complete. + The status of the operation can be checked using the `GET /api/slo/_bulk_delete/{taskId}` endpoint. + 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/bulk_delete_request.yaml' + responses: + '200': + description: Successful response + content: + application/json: + schema: + $ref: '../components/schemas/bulk_delete_response.yaml' + '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' diff --git a/x-pack/solutions/observability/plugins/slo/docs/openapi/slo/paths/s@{spaceid}@api@slos@_bulk_delete@{taskid}.yaml b/x-pack/solutions/observability/plugins/slo/docs/openapi/slo/paths/s@{spaceid}@api@slos@_bulk_delete@{taskid}.yaml new file mode 100644 index 0000000000000..86c297ecdd8c3 --- /dev/null +++ b/x-pack/solutions/observability/plugins/slo/docs/openapi/slo/paths/s@{spaceid}@api@slos@_bulk_delete@{taskid}.yaml @@ -0,0 +1,43 @@ +get: + summary: Retrieve the status of the bulk deletion + operationId: bulkDeleteStatusOp + description: > + Retrieve the status of the bulk deletion operation for SLOs. + This endpoint returns the status of the bulk deletion operation, including whether it is completed and the results of the operation. + tags: + - slo + parameters: + - $ref: ../components/headers/kbn_xsrf.yaml + - $ref: ../components/parameters/space_id.yaml + - name: taskId + in: path + description: The task id of the bulk delete operation + required: true + schema: + type: string + example: 8853df00-ae2e-11ed-90af-09bb6422b258 + responses: + '200': + description: Successful response + content: + application/json: + schema: + $ref: '../components/schemas/bulk_delete_status_response.yaml' + '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' diff --git a/x-pack/solutions/observability/plugins/slo/server/plugin.ts b/x-pack/solutions/observability/plugins/slo/server/plugin.ts index 07fae1604fc8d..cc6f0ea295d9d 100644 --- a/x-pack/solutions/observability/plugins/slo/server/plugin.ts +++ b/x-pack/solutions/observability/plugins/slo/server/plugin.ts @@ -36,6 +36,7 @@ import { KibanaSavedObjectsSLORepository, } from './services'; import { DefaultSummaryTransformGenerator } from './services/summary_transform_generator/summary_transform_generator'; +import { BulkDeleteTask } from './services/tasks/bulk_delete/bulk_delete_task'; import { SloOrphanSummaryCleanupTask } from './services/tasks/orphan_summary_cleanup_task'; import { TempSummaryCleanupTask } from './services/tasks/temp_summary_cleanup_task'; import { createTransformGenerators } from './services/transform_generators'; @@ -140,7 +141,7 @@ export class SLOPlugin registerSloUsageCollector(plugins.usageCollection); - const routeHandlerPlugins = mapValues(plugins, (value, key) => { + const mappedPlugins = mapValues(plugins, (value, key) => { return { setup: value, start: () => @@ -154,7 +155,7 @@ export class SLOPlugin core, dependencies: { corePlugins: core, - plugins: routeHandlerPlugins, + plugins: mappedPlugins, config: { isServerless: this.isServerless, }, @@ -232,6 +233,12 @@ export class SLOPlugin config: this.config, }); + new BulkDeleteTask({ + core, + plugins: mappedPlugins, + logFactory: this.initContext.logger, + }); + return {}; } diff --git a/x-pack/solutions/observability/plugins/slo/server/routes/slo/bulk_delete.ts b/x-pack/solutions/observability/plugins/slo/server/routes/slo/bulk_delete.ts new file mode 100644 index 0000000000000..21a780bc9e1b6 --- /dev/null +++ b/x-pack/solutions/observability/plugins/slo/server/routes/slo/bulk_delete.ts @@ -0,0 +1,81 @@ +/* + * 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 { + BulkDeleteStatusResponse, + bulkDeleteParamsSchema, + bulkDeleteStatusParamsSchema, +} from '@kbn/slo-schema'; +import { v4 } from 'uuid'; +import { TYPE } from '../../services/tasks/bulk_delete/bulk_delete_task'; +import { createSloServerRoute } from '../create_slo_server_route'; +import { assertPlatinumLicense } from './utils/assert_platinum_license'; + +export const bulkDeleteSLORoute = createSloServerRoute({ + endpoint: 'POST /api/observability/slos/_bulk_delete 2023-10-31', + options: { access: 'public' }, + security: { + authz: { + requiredPrivileges: ['slo_write'], + }, + }, + params: bulkDeleteParamsSchema, + handler: async ({ request, params, plugins }) => { + await assertPlatinumLicense(plugins); + const taskManager = await plugins.taskManager.start(); + + const taskId = v4(); + await taskManager.ensureScheduled( + { + id: taskId, + taskType: TYPE, + scope: ['observability', 'slo'], + state: {}, + runAt: new Date(Date.now() + 3 * 1000), + params: params.body, + }, + { request } + ); + + return { taskId }; + }, +}); + +export const getBulkDeleteStatusRoute = createSloServerRoute({ + endpoint: 'GET /api/observability/slos/_bulk_delete/{taskId} 2023-10-31', + options: { access: 'public' }, + security: { + authz: { + requiredPrivileges: ['slo_write'], + }, + }, + params: bulkDeleteStatusParamsSchema, + handler: async ({ params, plugins }) => { + await assertPlatinumLicense(plugins); + + const taskManager = await plugins.taskManager.start(); + const task = await taskManager.get(params.path.taskId).catch(() => undefined); + + if (!task) { + return { + isDone: true, + results: [], + error: 'Task not found', + } satisfies BulkDeleteStatusResponse; + } + + if (!task.state.isDone) { + return { isDone: false, results: [] } satisfies BulkDeleteStatusResponse; + } + + return { + isDone: task.state.isDone, + results: task.state.results, + error: task.state.error, + } satisfies BulkDeleteStatusResponse; + }, +}); diff --git a/x-pack/solutions/observability/plugins/slo/server/routes/slo/purge_rollup_data.ts b/x-pack/solutions/observability/plugins/slo/server/routes/slo/purge_rollup_data.ts index 6031a0d7cdb24..0ab74f924ec01 100644 --- a/x-pack/solutions/observability/plugins/slo/server/routes/slo/purge_rollup_data.ts +++ b/x-pack/solutions/observability/plugins/slo/server/routes/slo/purge_rollup_data.ts @@ -21,11 +21,8 @@ export const bulkPurgeRollupRoute = createSloServerRoute({ handler: async ({ request, context, params, logger, plugins, getScopedClients }) => { await assertPlatinumLicense(plugins); - const { repository } = await getScopedClients({ request, logger }); - - const core = await context.core; - const esClient = core.elasticsearch.client.asCurrentUser; - const purgeRollupData = new BulkPurgeRollupData(esClient, repository); + const { repository, scopedClusterClient } = await getScopedClients({ request, logger }); + const purgeRollupData = new BulkPurgeRollupData(scopedClusterClient.asCurrentUser, repository); return purgeRollupData.execute(params.body); }, diff --git a/x-pack/solutions/observability/plugins/slo/server/routes/slo/route.ts b/x-pack/solutions/observability/plugins/slo/server/routes/slo/route.ts index a800c45af409d..37e6e23cf029e 100644 --- a/x-pack/solutions/observability/plugins/slo/server/routes/slo/route.ts +++ b/x-pack/solutions/observability/plugins/slo/server/routes/slo/route.ts @@ -27,6 +27,7 @@ import { getSLOSuggestionsRoute } from './get_suggestions'; import { putSloSettings } from './put_slo_settings'; import { resetSLORoute } from './reset_slo'; import { getSLOStatsOverview } from './get_slo_stats_overview'; +import { bulkDeleteSLORoute, getBulkDeleteStatusRoute } from './bulk_delete'; import { bulkPurgeRollupRoute } from './purge_rollup_data'; export const getSloRouteRepository = (isServerless?: boolean) => { @@ -54,5 +55,7 @@ export const getSloRouteRepository = (isServerless?: boolean) => { ...findSLOGroupsRoute, ...getSLOSuggestionsRoute, ...getSLOStatsOverview, + ...bulkDeleteSLORoute, + ...getBulkDeleteStatusRoute, }; }; diff --git a/x-pack/solutions/observability/plugins/slo/server/services/__snapshots__/delete_slo.test.ts.snap b/x-pack/solutions/observability/plugins/slo/server/services/__snapshots__/delete_slo.test.ts.snap index 6336288d3bfc5..f90c23ad1f78a 100644 --- a/x-pack/solutions/observability/plugins/slo/server/services/__snapshots__/delete_slo.test.ts.snap +++ b/x-pack/solutions/observability/plugins/slo/server/services/__snapshots__/delete_slo.test.ts.snap @@ -59,6 +59,7 @@ exports[`DeleteSLO happy path removes all resources associatde to the slo 4`] = "ignore": Array [ 404, ], + "signal": AbortSignal {}, }, ], ], @@ -90,6 +91,9 @@ exports[`DeleteSLO happy path removes all resources associatde to the slo 5`] = "slices": "auto", "wait_for_completion": false, }, + Object { + "signal": AbortSignal {}, + }, ], Array [ Object { @@ -104,10 +108,13 @@ exports[`DeleteSLO happy path removes all resources associatde to the slo 5`] = }, }, }, - "refresh": true, + "refresh": false, "slices": "auto", "wait_for_completion": false, }, + Object { + "signal": AbortSignal {}, + }, ], ], "results": Array [ @@ -146,6 +153,9 @@ exports[`DeleteSLO happy path removes all resources associatde to the slo 7`] = "calls": Array [ Array [ "irrelevant", + Object { + "ignoreNotFound": true, + }, ], ], "results": Array [ diff --git a/x-pack/solutions/observability/plugins/slo/server/services/create_slo.ts b/x-pack/solutions/observability/plugins/slo/server/services/create_slo.ts index dc00f11530605..1513d4b1f93cf 100644 --- a/x-pack/solutions/observability/plugins/slo/server/services/create_slo.ts +++ b/x-pack/solutions/observability/plugins/slo/server/services/create_slo.ts @@ -54,7 +54,7 @@ export class CreateSLO { const rollbackOperations = []; const createPromise = this.repository.create(slo); - rollbackOperations.push(() => this.repository.deleteById(slo.id, true)); + rollbackOperations.push(() => this.repository.deleteById(slo.id, { ignoreNotFound: true })); const rollupTransformId = getSLOTransformId(slo.id, slo.revision); const summaryTransformId = getSLOSummaryTransformId(slo.id, slo.revision); diff --git a/x-pack/solutions/observability/plugins/slo/server/services/delete_slo.ts b/x-pack/solutions/observability/plugins/slo/server/services/delete_slo.ts index 31371e1a7bb78..e4093c54884d8 100644 --- a/x-pack/solutions/observability/plugins/slo/server/services/delete_slo.ts +++ b/x-pack/solutions/observability/plugins/slo/server/services/delete_slo.ts @@ -14,93 +14,118 @@ import { getSLOTransformId, getWildcardPipelineId, } from '../../common/constants'; -import { SLODefinition } from '../domain/models'; import { retryTransientEsErrors } from '../utils/retry'; import { SLORepository } from './slo_repository'; import { TransformManager } from './transform_manager'; +interface Options { + skipDataDeletion: boolean; + skipRuleDeletion: boolean; +} + export class DeleteSLO { constructor( private repository: SLORepository, private transformManager: TransformManager, private summaryTransformManager: TransformManager, private scopedClusterClient: IScopedClusterClient, - private rulesClient: RulesClientApi + private rulesClient: RulesClientApi, + private abortController: AbortController = new AbortController() ) {} - public async execute(sloId: string): Promise { + public async execute( + sloId: string, + options: Options = { + skipDataDeletion: false, + skipRuleDeletion: false, + } + ): Promise { const slo = await this.repository.findById(sloId); - await Promise.all([this.deleteSummaryTransform(slo), this.deleteRollupTransform(slo)]); - - await retryTransientEsErrors(() => - this.scopedClusterClient.asSecondaryAuthUser.ingest.deletePipeline( - { id: getWildcardPipelineId(slo.id, slo.revision) }, - { ignore: [404] } - ) - ); + // First delete the linked resources before deleting the data + const rollupTransformId = getSLOTransformId(slo.id, slo.revision); + const summaryTransformId = getSLOSummaryTransformId(slo.id, slo.revision); await Promise.all([ - this.deleteRollupData(slo.id), - this.deleteSummaryData(slo.id), - this.deleteAssociatedRules(slo.id), - this.repository.deleteById(slo.id), + this.transformManager.uninstall(rollupTransformId), + this.summaryTransformManager.uninstall(summaryTransformId), + retryTransientEsErrors(() => + this.scopedClusterClient.asSecondaryAuthUser.ingest.deletePipeline( + { id: getWildcardPipelineId(slo.id, slo.revision) }, + { ignore: [404], signal: this.abortController.signal } + ) + ), + this.repository.deleteById(slo.id, { ignoreNotFound: true }), ]); - } - private async deleteRollupTransform(slo: SLODefinition) { - const rollupTransformId = getSLOTransformId(slo.id, slo.revision); - await this.transformManager.uninstall(rollupTransformId); + await Promise.all([ + this.deleteRollupData(slo.id, options.skipDataDeletion), + this.deleteSummaryData(slo.id, options.skipDataDeletion), + this.deleteAssociatedRules(slo.id, options.skipRuleDeletion), + ]); } - private async deleteSummaryTransform(slo: SLODefinition) { - const summaryTransformId = getSLOSummaryTransformId(slo.id, slo.revision); - await this.summaryTransformManager.uninstall(summaryTransformId); - } + private async deleteRollupData(sloId: string, skip: boolean): Promise { + if (skip) { + return; + } - private async deleteRollupData(sloId: string): Promise { - await this.scopedClusterClient.asCurrentUser.deleteByQuery({ - index: SLI_DESTINATION_INDEX_PATTERN, - wait_for_completion: false, - conflicts: 'proceed', - slices: 'auto', - query: { - bool: { - filter: { - term: { - 'slo.id': sloId, + await this.scopedClusterClient.asCurrentUser.deleteByQuery( + { + index: SLI_DESTINATION_INDEX_PATTERN, + wait_for_completion: false, + conflicts: 'proceed', + slices: 'auto', + query: { + bool: { + filter: { + term: { + 'slo.id': sloId, + }, }, }, }, }, - }); + { signal: this.abortController.signal } + ); } - private async deleteSummaryData(sloId: string): Promise { - await this.scopedClusterClient.asCurrentUser.deleteByQuery({ - index: SUMMARY_DESTINATION_INDEX_PATTERN, - refresh: true, - wait_for_completion: false, - conflicts: 'proceed', - slices: 'auto', - query: { - bool: { - filter: { - term: { - 'slo.id': sloId, + private async deleteSummaryData(sloId: string, skip: boolean): Promise { + if (skip) { + return; + } + + await this.scopedClusterClient.asCurrentUser.deleteByQuery( + { + index: SUMMARY_DESTINATION_INDEX_PATTERN, + refresh: false, + wait_for_completion: false, + conflicts: 'proceed', + slices: 'auto', + query: { + bool: { + filter: { + term: { + 'slo.id': sloId, + }, }, }, }, }, - }); + { signal: this.abortController.signal } + ); } - private async deleteAssociatedRules(sloId: string): Promise { + private async deleteAssociatedRules(sloId: string, skip: boolean): Promise { + if (skip) { + return; + } + try { await this.rulesClient.bulkDeleteRules({ filter: `alert.attributes.params.sloId:${sloId}`, }); } catch (err) { - // no-op: bulkDeleteRules throws if no rules are found. + // no-op } } } diff --git a/x-pack/solutions/observability/plugins/slo/server/services/slo_repository.ts b/x-pack/solutions/observability/plugins/slo/server/services/slo_repository.ts index e6533981e4392..5530b313f88e4 100644 --- a/x-pack/solutions/observability/plugins/slo/server/services/slo_repository.ts +++ b/x-pack/solutions/observability/plugins/slo/server/services/slo_repository.ts @@ -21,7 +21,7 @@ export interface SLORepository { update(slo: SLODefinition): Promise; findAllByIds(ids: string[]): Promise; findById(id: string): Promise; - deleteById(id: string, ignoreNotFound?: boolean): Promise; + deleteById(id: string, options?: { ignoreNotFound?: boolean }): Promise; search( search: string, pagination: Pagination, @@ -90,7 +90,7 @@ export class KibanaSavedObjectsSLORepository implements SLORepository { return slo; } - async deleteById(id: string, ignoreNotFound = false): Promise { + async deleteById(id: string, { ignoreNotFound = false }): Promise { const response = await this.soClient.find({ type: SO_SLO_TYPE, page: 1, diff --git a/x-pack/solutions/observability/plugins/slo/server/services/summay_transform_manager.ts b/x-pack/solutions/observability/plugins/slo/server/services/summay_transform_manager.ts index 078a56b29ca9f..1e79b80ce8860 100644 --- a/x-pack/solutions/observability/plugins/slo/server/services/summay_transform_manager.ts +++ b/x-pack/solutions/observability/plugins/slo/server/services/summay_transform_manager.ts @@ -19,14 +19,18 @@ export class DefaultSummaryTransformManager implements TransformManager { constructor( private generator: SummaryTransformGenerator, private scopedClusterClient: IScopedClusterClient, - private logger: Logger + private logger: Logger, + private abortController: AbortController = new AbortController() ) {} async install(slo: SLODefinition): Promise { const transformParams = await this.generator.generate(slo); try { await retryTransientEsErrors( - () => this.scopedClusterClient.asSecondaryAuthUser.transform.putTransform(transformParams), + () => + this.scopedClusterClient.asSecondaryAuthUser.transform.putTransform(transformParams, { + signal: this.abortController.signal, + }), { logger: this.logger, } @@ -51,9 +55,10 @@ export class DefaultSummaryTransformManager implements TransformManager { try { await retryTransientEsErrors( () => - this.scopedClusterClient.asSecondaryAuthUser.transform.previewTransform({ - transform_id: transformId, - }), + this.scopedClusterClient.asSecondaryAuthUser.transform.previewTransform( + { transform_id: transformId }, + { signal: this.abortController.signal } + ), { logger: this.logger } ); } catch (err) { @@ -68,7 +73,7 @@ export class DefaultSummaryTransformManager implements TransformManager { () => this.scopedClusterClient.asSecondaryAuthUser.transform.startTransform( { transform_id: transformId }, - { ignore: [409] } + { ignore: [409], signal: this.abortController.signal } ), { logger: this.logger, @@ -86,7 +91,7 @@ export class DefaultSummaryTransformManager implements TransformManager { () => this.scopedClusterClient.asSecondaryAuthUser.transform.stopTransform( { transform_id: transformId, wait_for_completion: true, force: true }, - { ignore: [404] } + { ignore: [404], signal: this.abortController.signal } ), { logger: this.logger } ); @@ -102,7 +107,7 @@ export class DefaultSummaryTransformManager implements TransformManager { () => this.scopedClusterClient.asSecondaryAuthUser.transform.deleteTransform( { transform_id: transformId, force: true }, - { ignore: [404] } + { ignore: [404], signal: this.abortController.signal } ), { logger: this.logger } ); @@ -118,7 +123,7 @@ export class DefaultSummaryTransformManager implements TransformManager { () => this.scopedClusterClient.asSecondaryAuthUser.transform.getTransform( { transform_id: transformId }, - { ignore: [404] } + { ignore: [404], signal: this.abortController.signal } ), { logger: this.logger } ); diff --git a/x-pack/solutions/observability/plugins/slo/server/services/tasks/bulk_delete/bulk_delete_task.ts b/x-pack/solutions/observability/plugins/slo/server/services/tasks/bulk_delete/bulk_delete_task.ts new file mode 100644 index 0000000000000..d46d6f93cc057 --- /dev/null +++ b/x-pack/solutions/observability/plugins/slo/server/services/tasks/bulk_delete/bulk_delete_task.ts @@ -0,0 +1,136 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { type CoreSetup, type Logger, type LoggerFactory } from '@kbn/core/server'; +import { BulkDeleteParams, BulkDeleteStatusResponse } from '@kbn/slo-schema'; +import { RunContext } from '@kbn/task-manager-plugin/server'; +import { IndicatorTypes } from '../../../domain/models'; +import { SLOPluginSetupDependencies, SLOPluginStartDependencies } from '../../../types'; +import { DeleteSLO } from '../../delete_slo'; +import { KibanaSavedObjectsSLORepository } from '../../slo_repository'; +import { DefaultSummaryTransformGenerator } from '../../summary_transform_generator/summary_transform_generator'; +import { DefaultSummaryTransformManager } from '../../summay_transform_manager'; +import { TransformGenerator } from '../../transform_generators'; +import { DefaultTransformManager } from '../../transform_manager'; +import { runBulkDelete } from './run_bulk_delete'; + +export const TYPE = 'slo:bulk-delete-task'; + +interface TaskSetupContract { + core: CoreSetup; + logFactory: LoggerFactory; + plugins: { + [key in keyof SLOPluginSetupDependencies]: { + setup: Required[key]; + }; + } & { + [key in keyof SLOPluginStartDependencies]: { + start: () => Promise[key]>; + }; + }; +} + +export class BulkDeleteTask { + private abortController = new AbortController(); + private logger: Logger; + + constructor(setupContract: TaskSetupContract) { + const { core, plugins, logFactory } = setupContract; + this.logger = logFactory.get(TYPE); + + this.logger.debug('Registering task with [10m] timeout'); + + plugins.taskManager.setup.registerTaskDefinitions({ + [TYPE]: { + title: 'SLO bulk delete', + timeout: '10m', + maxAttempts: 1, + createTaskRunner: ({ taskInstance, fakeRequest }: RunContext) => { + return { + run: async () => { + this.logger.debug(`starting bulk delete operation`); + + if (!fakeRequest) { + this.logger.debug('fakeRequest is not defined'); + return; + } + + const state = taskInstance.state as BulkDeleteStatusResponse; + if (state.isDone) { + // The task was done in the previous run, + // we only rescheduled it once for keeping an ephemeral state for the user + return; + } + + const [coreStart, pluginStart] = await core.getStartServices(); + + const scopedClusterClient = coreStart.elasticsearch.client.asScoped(fakeRequest); + const scopedSoClient = coreStart.savedObjects.getScopedClient(fakeRequest); + const rulesClient = await pluginStart.alerting.getRulesClientWithRequest(fakeRequest); + + const repository = new KibanaSavedObjectsSLORepository(scopedSoClient, this.logger); + const transformManager = new DefaultTransformManager( + {} as Record, + scopedClusterClient, + this.logger, + this.abortController + ); + const summaryTransformManager = new DefaultSummaryTransformManager( + new DefaultSummaryTransformGenerator(), + scopedClusterClient, + this.logger, + this.abortController + ); + + const deleteSLO = new DeleteSLO( + repository, + transformManager, + summaryTransformManager, + scopedClusterClient, + rulesClient, + this.abortController + ); + + try { + const params = taskInstance.params as BulkDeleteParams; + + const results = await runBulkDelete(params, { + deleteSLO, + scopedClusterClient, + rulesClient, + logger: this.logger, + abortController: this.abortController, + }); + + return { + runAt: new Date(Date.now() + 60 * 60 * 1000), + state: { + isDone: true, + results, + } satisfies BulkDeleteStatusResponse, + }; + } catch (err) { + this.logger.debug(`Error: ${err}`); + return { + runAt: new Date(Date.now() + 60 * 60 * 1000), + state: { + isDone: true, + error: err.message, + } satisfies BulkDeleteStatusResponse, + }; + } + }, + + cancel: async () => { + this.abortController.abort('Timed out'); + }, + }; + }, + }, + }); + } +} diff --git a/x-pack/solutions/observability/plugins/slo/server/services/tasks/bulk_delete/run_bulk_delete.ts b/x-pack/solutions/observability/plugins/slo/server/services/tasks/bulk_delete/run_bulk_delete.ts new file mode 100644 index 0000000000000..f6d9672248049 --- /dev/null +++ b/x-pack/solutions/observability/plugins/slo/server/services/tasks/bulk_delete/run_bulk_delete.ts @@ -0,0 +1,114 @@ +/* + * 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 { RulesClientApi } from '@kbn/alerting-plugin/server/types'; +import { IScopedClusterClient, Logger } from '@kbn/core/server'; +import { BulkDeleteParams, BulkDeleteResult } from '@kbn/slo-schema'; +import pLimit from 'p-limit'; +import { + SLI_DESTINATION_INDEX_PATTERN, + SUMMARY_DESTINATION_INDEX_PATTERN, +} from '../../../../common/constants'; +import { DeleteSLO } from '../../delete_slo'; + +interface Dependencies { + scopedClusterClient: IScopedClusterClient; + rulesClient: RulesClientApi; + deleteSLO: DeleteSLO; + logger: Logger; + abortController: AbortController; +} + +export async function runBulkDelete( + params: BulkDeleteParams, + dependencies: Dependencies +): Promise { + const { scopedClusterClient, rulesClient, deleteSLO, logger, abortController } = dependencies; + + logger.debug(`Starting bulk deletion for SLO [${params.list.join(', ')}]`); + + const limiter = pLimit(3); + + const promises = params.list.map(async (sloId) => { + return limiter(async () => { + try { + await deleteSLO.execute(sloId, { skipRuleDeletion: true, skipDataDeletion: true }); + } catch (err) { + return { + id: sloId, + success: false, + error: err.message, + }; + } + + return { id: sloId, success: true }; + }); + }); + + const results = await Promise.all(promises); + + const itemsDeletedSuccessfully = results + .filter((result) => result.success === true) + .map((result) => result.id); + + try { + await Promise.all([ + scopedClusterClient.asCurrentUser.deleteByQuery( + { + index: SLI_DESTINATION_INDEX_PATTERN, + refresh: false, + wait_for_completion: false, + conflicts: 'proceed', + slices: 'auto', + query: { + bool: { + filter: { + terms: { + 'slo.id': itemsDeletedSuccessfully, + }, + }, + }, + }, + }, + { signal: abortController.signal } + ), + scopedClusterClient.asCurrentUser.deleteByQuery( + { + index: SUMMARY_DESTINATION_INDEX_PATTERN, + + refresh: false, + wait_for_completion: false, + conflicts: 'proceed', + slices: 'auto', + query: { + bool: { + filter: { + terms: { + 'slo.id': itemsDeletedSuccessfully, + }, + }, + }, + }, + }, + { signal: abortController.signal } + ), + ]); + } catch (err) { + logger.debug(`Error scheduling tasks for data deletion: ${err}`); + } + + try { + await rulesClient.bulkDeleteRules({ + filter: `alert.attributes.params.sloId:${itemsDeletedSuccessfully.join(' or ')}`, + }); + } catch (err) { + logger.debug(`Error deleting rules: ${err}`); + } + + logger.debug(`completed bulk deletion: [${itemsDeletedSuccessfully.join(',')}]`); + return results; +} diff --git a/x-pack/solutions/observability/plugins/slo/server/services/transform_manager.ts b/x-pack/solutions/observability/plugins/slo/server/services/transform_manager.ts index 0b3a892e72228..ea91b55baf9c6 100644 --- a/x-pack/solutions/observability/plugins/slo/server/services/transform_manager.ts +++ b/x-pack/solutions/observability/plugins/slo/server/services/transform_manager.ts @@ -28,7 +28,8 @@ export class DefaultTransformManager implements TransformManager { constructor( private generators: Record, private scopedClusterClient: IScopedClusterClient, - private logger: Logger + private logger: Logger, + private abortController: AbortController = new AbortController() ) {} async install(slo: SLODefinition): Promise { @@ -41,10 +42,11 @@ export class DefaultTransformManager implements TransformManager { const transformParams = await generator.getTransformParams(slo); try { await retryTransientEsErrors( - () => this.scopedClusterClient.asSecondaryAuthUser.transform.putTransform(transformParams), - { - logger: this.logger, - } + () => + this.scopedClusterClient.asSecondaryAuthUser.transform.putTransform(transformParams, { + signal: this.abortController.signal, + }), + { logger: this.logger } ); } catch (err) { this.logger.debug( @@ -74,9 +76,10 @@ export class DefaultTransformManager implements TransformManager { try { await retryTransientEsErrors( () => - this.scopedClusterClient.asSecondaryAuthUser.transform.previewTransform({ - transform_id: transformId, - }), + this.scopedClusterClient.asSecondaryAuthUser.transform.previewTransform( + { transform_id: transformId }, + { signal: this.abortController.signal } + ), { logger: this.logger } ); } catch (err) { @@ -91,7 +94,7 @@ export class DefaultTransformManager implements TransformManager { () => this.scopedClusterClient.asSecondaryAuthUser.transform.startTransform( { transform_id: transformId }, - { ignore: [409] } + { ignore: [409], signal: this.abortController.signal } ), { logger: this.logger } ); @@ -113,7 +116,7 @@ export class DefaultTransformManager implements TransformManager { force: true, allow_no_match: true, }, - { ignore: [404] } + { ignore: [404], signal: this.abortController.signal } ), { logger: this.logger } ); @@ -129,7 +132,7 @@ export class DefaultTransformManager implements TransformManager { () => this.scopedClusterClient.asSecondaryAuthUser.transform.deleteTransform( { transform_id: transformId, force: true }, - { ignore: [404] } + { ignore: [404], signal: this.abortController.signal } ), { logger: this.logger } ); @@ -145,7 +148,7 @@ export class DefaultTransformManager implements TransformManager { () => this.scopedClusterClient.asSecondaryAuthUser.transform.getTransform( { transform_id: transformId }, - { ignore: [404] } + { ignore: [404], signal: this.abortController.signal } ), { logger: this.logger } ); @@ -158,7 +161,7 @@ export class DefaultTransformManager implements TransformManager { private async scheduleNowTransform(transformId: TransformId) { this.scopedClusterClient.asSecondaryAuthUser.transform - .scheduleNowTransform({ transform_id: transformId }) + .scheduleNowTransform({ transform_id: transformId }, { signal: this.abortController.signal }) .then(() => { this.logger.debug(`SLO transform [${transformId}] scheduled now successfully`); }) diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/slo/bulk_delete.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/slo/bulk_delete.ts new file mode 100644 index 0000000000000..2b4f94f4c70bc --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/slo/bulk_delete.ts @@ -0,0 +1,84 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { cleanup, generate } from '@kbn/data-forge'; +import expect from '@kbn/expect'; +import { RoleCredentials } from '@kbn/ftr-common-functional-services'; +import { DeploymentAgnosticFtrProviderContext } from '../../../ftr_provider_context'; +import { DATA_FORGE_CONFIG } from './helpers/dataforge'; +import { DEFAULT_SLO } from './fixtures/slo'; + +export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { + const esClient = getService('es'); + const sloApi = getService('sloApi'); + const retry = getService('retry'); + const logger = getService('log'); + const samlAuth = getService('samlAuth'); + const dataViewApi = getService('dataViewApi'); + + const DATA_VIEW = 'kbn-data-forge-fake_hosts.fake_hosts-*'; + const DATA_VIEW_ID = 'data-view-id'; + + let adminRoleAuthc: RoleCredentials; + + describe('Bulk Delete SLO', function () { + before(async () => { + adminRoleAuthc = await samlAuth.createM2mApiKeyWithRoleScope('admin'); + + await generate({ client: esClient, config: DATA_FORGE_CONFIG, logger }); + + await dataViewApi.create({ + roleAuthc: adminRoleAuthc, + name: DATA_VIEW, + id: DATA_VIEW_ID, + title: DATA_VIEW, + }); + + await sloApi.deleteAllSLOs(adminRoleAuthc); + }); + + after(async () => { + await dataViewApi.delete({ roleAuthc: adminRoleAuthc, id: DATA_VIEW_ID }); + await cleanup({ client: esClient, config: DATA_FORGE_CONFIG, logger }); + await sloApi.deleteAllSLOs(adminRoleAuthc); + await samlAuth.invalidateM2mApiKeyWithRoleScope(adminRoleAuthc); + }); + + it('successfully processes the list of SLOs', async () => { + const { id: sloId } = await sloApi.create(DEFAULT_SLO, adminRoleAuthc); + + const response = await sloApi.bulkDelete({ list: [sloId, 'inexistant-slo'] }, adminRoleAuthc); + expect(response).to.have.property('taskId'); + + await retry.waitFor('task completion', async () => { + const status = await sloApi.bulkDeleteStatus(response.taskId, adminRoleAuthc); + return status.isDone === true; + }); + + const status = await sloApi.bulkDeleteStatus(response.taskId, adminRoleAuthc); + expect(status).eql({ + isDone: true, + results: [ + { + id: sloId, + success: true, + }, + { + id: 'inexistant-slo', + success: false, + error: `SLO [inexistant-slo] not found`, + }, + ], + }); + }); + + it('returns task not found', async () => { + const status = await sloApi.bulkDeleteStatus('inexistant', adminRoleAuthc); + expect(status).eql({ isDone: true, results: [], error: 'Task not found' }); + }); + }); +} diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/slo/index.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/slo/index.ts index 6d244b889a46f..31ccc819d6c29 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/slo/index.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/slo/index.ts @@ -16,5 +16,6 @@ export default function ({ loadTestFile }: DeploymentAgnosticFtrProviderContext) loadTestFile(require.resolve('./find_slo_definition')); loadTestFile(require.resolve('./reset_slo')); loadTestFile(require.resolve('./update_slo')); + loadTestFile(require.resolve('./bulk_delete')); }); } diff --git a/x-pack/test/api_integration/deployment_agnostic/services/slo_api.ts b/x-pack/test/api_integration/deployment_agnostic/services/slo_api.ts index ccbc410b5bd3c..42088fa5faad4 100644 --- a/x-pack/test/api_integration/deployment_agnostic/services/slo_api.ts +++ b/x-pack/test/api_integration/deployment_agnostic/services/slo_api.ts @@ -6,8 +6,13 @@ */ import { RoleCredentials } from '@kbn/ftr-common-functional-services'; -import { CreateSLOInput, FindSLODefinitionsResponse, UpdateSLOInput } from '@kbn/slo-schema'; import { StoredSLODefinition } from '@kbn/slo-plugin/server/domain/models/slo'; +import { + BulkDeleteInput, + CreateSLOInput, + FindSLODefinitionsResponse, + UpdateSLOInput, +} from '@kbn/slo-schema'; import { DeploymentAgnosticFtrProviderContext } from '../ftr_provider_context'; interface SavedObject> { @@ -73,6 +78,28 @@ export function SloApiProvider({ getService }: DeploymentAgnosticFtrProviderCont .expect(204); }, + async bulkDelete(params: BulkDeleteInput, roleAuthc: RoleCredentials) { + const { body: response } = await supertestWithoutAuth + .post(`/api/observability/slos/_bulk_delete`) + .set(roleAuthc.apiKeyHeader) + .set(samlAuth.getInternalRequestHeader()) + .send(params) + .expect(200); + + return response; + }, + + async bulkDeleteStatus(taskId: string, roleAuthc: RoleCredentials) { + const { body: response } = await supertestWithoutAuth + .get(`/api/observability/slos/_bulk_delete/${taskId}`) + .set(roleAuthc.apiKeyHeader) + .set(samlAuth.getInternalRequestHeader()) + .send() + .expect(200); + + return response; + }, + async get(id: string, roleAuthc: RoleCredentials) { const { body } = await supertestWithoutAuth .get(`/api/observability/slos/${id}`) diff --git a/x-pack/test/plugin_api_integration/test_suites/task_manager/check_registered_task_types.ts b/x-pack/test/plugin_api_integration/test_suites/task_manager/check_registered_task_types.ts index 6e2434999b381..b5b3c7e652bf9 100644 --- a/x-pack/test/plugin_api_integration/test_suites/task_manager/check_registered_task_types.ts +++ b/x-pack/test/plugin_api_integration/test_suites/task_manager/check_registered_task_types.ts @@ -178,6 +178,7 @@ export default function ({ getService }: FtrProviderContext) { 'security:telemetry-prebuilt-rule-alerts', 'security:telemetry-timelines', 'session_cleanup', + 'slo:bulk-delete-task', 'slo:temp-summary-cleanup-task', 'task_manager:delete_inactive_background_task_nodes', 'task_manager:mark_removed_tasks_as_unrecognized',