From b6de3b713dd4cd3996b40bce53c7f0f31b2bcf1f Mon Sep 17 00:00:00 2001 From: Maxim Kholod Date: Mon, 28 Oct 2024 12:18:48 +0100 Subject: [PATCH 1/9] [Cloud Security] add posture type for CSPM dashboard accounts link (#197633) ## Summary - fixes https://github.com/elastic/security-team/issues/10914 --- .../public/components/accounts_evaluated_widget.test.tsx | 1 + .../public/components/accounts_evaluated_widget.tsx | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/cloud_security_posture/public/components/accounts_evaluated_widget.test.tsx b/x-pack/plugins/cloud_security_posture/public/components/accounts_evaluated_widget.test.tsx index d81737fea1ec2..9f5f2ec85d021 100644 --- a/x-pack/plugins/cloud_security_posture/public/components/accounts_evaluated_widget.test.tsx +++ b/x-pack/plugins/cloud_security_posture/public/components/accounts_evaluated_widget.test.tsx @@ -45,6 +45,7 @@ describe('AccountsEvaluatedWidget', () => { expect(mockNavToFindings).toHaveBeenCalledWith( { 'cloud.provider': 'aws', + 'rule.benchmark.posture_type': 'cspm', }, ['cloud.account.name'] ); diff --git a/x-pack/plugins/cloud_security_posture/public/components/accounts_evaluated_widget.tsx b/x-pack/plugins/cloud_security_posture/public/components/accounts_evaluated_widget.tsx index 1d4f26274690d..5ae8a47a93e71 100644 --- a/x-pack/plugins/cloud_security_posture/public/components/accounts_evaluated_widget.tsx +++ b/x-pack/plugins/cloud_security_posture/public/components/accounts_evaluated_widget.tsx @@ -8,6 +8,7 @@ import React from 'react'; import { EuiFlexGroup, EuiFlexItem, useEuiTheme } from '@elastic/eui'; import { css } from '@emotion/react'; import { useNavigateFindings } from '@kbn/cloud-security-posture/src/hooks/use_navigate_findings'; +import { CSPM_POLICY_TEMPLATE } from '@kbn/cloud-security-posture-common'; import { CLOUD_PROVIDERS, getBenchmarkApplicableTo } from '../../common/utils/helpers'; import { CIS_AWS, CIS_GCP, CIS_AZURE, CIS_K8S, CIS_EKS } from '../../common/constants'; import { CISBenchmarkIcon } from './cis_benchmark_icon'; @@ -61,7 +62,10 @@ export const AccountsEvaluatedWidget = ({ const navToFindings = useNavigateFindings(); const navToFindingsByCloudProvider = (provider: string) => { - navToFindings({ 'cloud.provider': provider }, [FINDINGS_GROUPING_OPTIONS.CLOUD_ACCOUNT_NAME]); + navToFindings( + { 'cloud.provider': provider, 'rule.benchmark.posture_type': CSPM_POLICY_TEMPLATE }, + [FINDINGS_GROUPING_OPTIONS.CLOUD_ACCOUNT_NAME] + ); }; const navToFindingsByCisBenchmark = (cisBenchmark: string) => { From 97f227ef714b3ffe83cbd6a9db5db9d11e6d23fd Mon Sep 17 00:00:00 2001 From: Mykola Harmash Date: Mon, 28 Oct 2024 12:21:07 +0100 Subject: [PATCH 2/9] [Observability Onboarding] Show search bar even when category is not selected (#197825) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes https://github.com/elastic/observability-dev/issues/4065 🔒 Fixes an issue when search bar is not visible unless a category is selected. ![CleanShot 2024-10-25 at 14 53 44@2x](https://github.com/user-attachments/assets/774d2fe2-e4f0-4a46-a851-a0f756a96b12) --- .../onboarding_flow_form.tsx | 55 ++++++++++--------- 1 file changed, 28 insertions(+), 27 deletions(-) diff --git a/x-pack/plugins/observability_solution/observability_onboarding/public/application/onboarding_flow_form/onboarding_flow_form.tsx b/x-pack/plugins/observability_solution/observability_onboarding/public/application/onboarding_flow_form/onboarding_flow_form.tsx index 9a46cf885b285..eb5ee3bc92369 100644 --- a/x-pack/plugins/observability_solution/observability_onboarding/public/application/onboarding_flow_form/onboarding_flow_form.tsx +++ b/x-pack/plugins/observability_solution/observability_onboarding/public/application/onboarding_flow_form/onboarding_flow_form.tsx @@ -316,33 +316,34 @@ export const OnboardingFlowForm: FunctionComponent = () => { flowCategory={searchParams.get('category')} /> -
- - - - - - - - card.type === 'virtual' && !card.isCollectionCard - ) - .concat(virtualSearchResults)} - excludePackageIdList={searchExcludePackageIdList} - joinCardLists - /> -
+ + +
+ + + + + + + + card.type === 'virtual' && !card.isCollectionCard + ) + .concat(virtualSearchResults)} + excludePackageIdList={searchExcludePackageIdList} + joinCardLists + />
); From 73c22a50fda298537f28bd000731b0584503e7ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Georgiana-Andreea=20Onolea=C8=9B=C4=83?= Date: Mon, 28 Oct 2024 13:39:32 +0200 Subject: [PATCH 3/9] [ResponseOps][Cases] Miscount of total numbers of alerts in telemetry (#196112) Closes https://github.com/elastic/kibana/issues/177208 ## Summary Problem: - the metrics collected in telemetry for alerts don't count the total number of alerts on a case correctly. Solution: - added new aggregation function: getUniqueAlertCommentsCountQuery, which is now responsible for defining the cardinality aggregation for counting unique alert comments by alertId. - in the aggs section of the savedObjectsClient.find, the new cardinality aggregation query was added - the total number of alerts is updated to be the result extracted from the new aggregation Example: ![Screenshot 2024-10-22 at 15 20 40](https://github.com/user-attachments/assets/c418c82e-2e35-4c7f-969d-7f4f25bdbc9d) - in the telemetry object, we have the following info: Screenshot 2024-10-22 at 15 21 40 --------- Co-authored-by: Antonio --- .../server/telemetry/queries/alerts.test.ts | 40 ++- .../cases/server/telemetry/queries/alerts.ts | 7 +- .../server/telemetry/queries/utils.test.ts | 228 ++++++++++++++++++ .../cases/server/telemetry/queries/utils.ts | 122 +++++++++- .../plugins/cases/server/telemetry/types.ts | 4 + .../tests/common/telemetry.ts | 51 +++- 6 files changed, 438 insertions(+), 14 deletions(-) diff --git a/x-pack/plugins/cases/server/telemetry/queries/alerts.test.ts b/x-pack/plugins/cases/server/telemetry/queries/alerts.test.ts index 11636b50ebd4e..fd00aea939dc8 100644 --- a/x-pack/plugins/cases/server/telemetry/queries/alerts.test.ts +++ b/x-pack/plugins/cases/server/telemetry/queries/alerts.test.ts @@ -17,19 +17,20 @@ describe('alerts', () => { const telemetrySavedObjectsClient = new TelemetrySavedObjectsClient(savedObjectsClient); savedObjectsClient.find.mockResolvedValue({ - total: 5, + total: 3, saved_objects: [], per_page: 1, page: 1, aggregations: { counts: { buckets: [ - { doc_count: 1, key: 1 }, - { doc_count: 2, key: 2 }, - { doc_count: 3, key: 3 }, + { topAlertsPerBucket: { value: 12 } }, + { topAlertsPerBucket: { value: 5 } }, + { topAlertsPerBucket: { value: 3 } }, ], }, references: { cases: { max: { value: 1 } } }, + uniqueAlertCommentsCount: { value: 5 }, }, }); @@ -42,12 +43,13 @@ describe('alerts', () => { savedObjectsClient: telemetrySavedObjectsClient, logger, }); + expect(res).toEqual({ all: { total: 5, daily: 3, - weekly: 2, - monthly: 1, + weekly: 5, + monthly: 12, maxOnACase: 1, }, }); @@ -76,6 +78,13 @@ describe('alerts', () => { }, ], }, + aggregations: { + topAlertsPerBucket: { + cardinality: { + field: 'cases-comments.attributes.alertId', + }, + }, + }, }, references: { aggregations: { @@ -85,10 +94,22 @@ describe('alerts', () => { terms: { field: 'cases-comments.references.id', }, + aggregations: { + reverse: { + reverse_nested: {}, + aggregations: { + topAlerts: { + cardinality: { + field: 'cases-comments.attributes.alertId', + }, + }, + }, + }, + }, }, max: { max_bucket: { - buckets_path: 'ids._count', + buckets_path: 'ids>reverse.topAlerts', }, }, }, @@ -103,6 +124,11 @@ describe('alerts', () => { path: 'cases-comments.references', }, }, + uniqueAlertCommentsCount: { + cardinality: { + field: 'cases-comments.attributes.alertId', + }, + }, }, filter: { arguments: [ diff --git a/x-pack/plugins/cases/server/telemetry/queries/alerts.ts b/x-pack/plugins/cases/server/telemetry/queries/alerts.ts index 96aaec211acb8..88a9c25c88c3d 100644 --- a/x-pack/plugins/cases/server/telemetry/queries/alerts.ts +++ b/x-pack/plugins/cases/server/telemetry/queries/alerts.ts @@ -5,17 +5,14 @@ * 2.0. */ -import { CASE_COMMENT_SAVED_OBJECT } from '../../../common/constants'; import type { CasesTelemetry, CollectTelemetryDataParams } from '../types'; -import { getCountsAndMaxData, getOnlyAlertsCommentsFilter } from './utils'; +import { getCountsAndMaxAlertsData } from './utils'; export const getAlertsTelemetryData = async ({ savedObjectsClient, }: CollectTelemetryDataParams): Promise => { - const res = await getCountsAndMaxData({ + const res = await getCountsAndMaxAlertsData({ savedObjectsClient, - savedObjectType: CASE_COMMENT_SAVED_OBJECT, - filter: getOnlyAlertsCommentsFilter(), }); return res; diff --git a/x-pack/plugins/cases/server/telemetry/queries/utils.test.ts b/x-pack/plugins/cases/server/telemetry/queries/utils.test.ts index 6c66c5aab81c7..b4b18f231eb6a 100644 --- a/x-pack/plugins/cases/server/telemetry/queries/utils.test.ts +++ b/x-pack/plugins/cases/server/telemetry/queries/utils.test.ts @@ -16,10 +16,12 @@ import type { import { findValueInBuckets, getAggregationsBuckets, + getAlertsCountsFromBuckets, getAttachmentsFrameworkStats, getBucketFromAggregation, getConnectorsCardinalityAggregationQuery, getCountsAggregationQuery, + getCountsAndMaxAlertsData, getCountsAndMaxData, getCountsFromBuckets, getCustomFieldsTelemetry, @@ -28,6 +30,7 @@ import { getOnlyConnectorsFilter, getReferencesAggregationQuery, getSolutionValues, + getUniqueAlertCommentsCountQuery, } from './utils'; import { TelemetrySavedObjectsClient } from '../telemetry_saved_objects_client'; @@ -994,6 +997,63 @@ describe('utils', () => { }); }); + describe('getAlertsCountsFromBuckets', () => { + it('returns the correct counts', () => { + const buckets = [ + { topAlertsPerBucket: { value: 12 } }, + { topAlertsPerBucket: { value: 5 } }, + { topAlertsPerBucket: { value: 3 } }, + ]; + + expect(getAlertsCountsFromBuckets(buckets)).toEqual({ + daily: 3, + weekly: 5, + monthly: 12, + }); + }); + + it('returns zero counts when the bucket does not have the topAlertsPerBucket field', () => { + const buckets = [{}]; + // @ts-expect-error + expect(getAlertsCountsFromBuckets(buckets)).toEqual({ + daily: 0, + weekly: 0, + monthly: 0, + }); + }); + + it('returns zero counts when the bucket is undefined', () => { + // @ts-expect-error + expect(getAlertsCountsFromBuckets(undefined)).toEqual({ + daily: 0, + weekly: 0, + monthly: 0, + }); + }); + + it('returns zero counts when the topAlertsPerBucket field is missing in some buckets', () => { + const buckets = [{ doc_count: 1, key: 1, topAlertsPerBucket: { value: 5 } }, {}, {}]; + // @ts-expect-error + expect(getAlertsCountsFromBuckets(buckets)).toEqual({ + daily: 0, + weekly: 0, + monthly: 5, + }); + }); + }); + + describe('getUniqueAlertCommentsCountQuery', () => { + it('returns the correct query', () => { + expect(getUniqueAlertCommentsCountQuery()).toEqual({ + uniqueAlertCommentsCount: { + cardinality: { + field: 'cases-comments.attributes.alertId', + }, + }, + }); + }); + }); + describe('getCountsAndMaxData', () => { const savedObjectsClient = savedObjectsRepositoryMock.create(); savedObjectsClient.find.mockResolvedValue({ @@ -1125,6 +1185,174 @@ describe('utils', () => { }); }); + describe('getCountsAndMaxAlertsData', () => { + const savedObjectsClient = savedObjectsRepositoryMock.create(); + savedObjectsClient.find.mockResolvedValue({ + total: 3, + saved_objects: [], + per_page: 1, + page: 1, + aggregations: { + counts: { + buckets: [ + { doc_count: 1, key: 1, topAlertsPerBucket: { value: 5 } }, + { doc_count: 2, key: 2, topAlertsPerBucket: { value: 3 } }, + { doc_count: 3, key: 3, topAlertsPerBucket: { value: 1 } }, + ], + }, + references: { cases: { max: { value: 1 } } }, + uniqueAlertCommentsCount: { value: 5 }, + }, + }); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('returns the correct counts and max data', async () => { + const telemetrySavedObjectsClient = new TelemetrySavedObjectsClient(savedObjectsClient); + + const res = await getCountsAndMaxAlertsData({ + savedObjectsClient: telemetrySavedObjectsClient, + }); + expect(res).toEqual({ + all: { + total: 5, + daily: 1, + weekly: 3, + monthly: 5, + maxOnACase: 1, + }, + }); + }); + + it('returns zero data if the response aggregation is not as expected', async () => { + const telemetrySavedObjectsClient = new TelemetrySavedObjectsClient(savedObjectsClient); + savedObjectsClient.find.mockResolvedValue({ + total: 5, + saved_objects: [], + per_page: 1, + page: 1, + }); + + const res = await getCountsAndMaxAlertsData({ + savedObjectsClient: telemetrySavedObjectsClient, + }); + expect(res).toEqual({ + all: { + total: 0, + daily: 0, + weekly: 0, + monthly: 0, + maxOnACase: 0, + }, + }); + }); + + it('should call find with correct arguments', async () => { + const telemetrySavedObjectsClient = new TelemetrySavedObjectsClient(savedObjectsClient); + + await getCountsAndMaxAlertsData({ + savedObjectsClient: telemetrySavedObjectsClient, + }); + + expect(savedObjectsClient.find).toBeCalledWith({ + aggs: { + counts: { + date_range: { + field: 'cases-comments.attributes.created_at', + format: 'dd/MM/YYYY', + ranges: [ + { + from: 'now-1d', + to: 'now', + }, + { + from: 'now-1w', + to: 'now', + }, + { + from: 'now-1M', + to: 'now', + }, + ], + }, + aggregations: { + topAlertsPerBucket: { + cardinality: { + field: 'cases-comments.attributes.alertId', + }, + }, + }, + }, + references: { + aggregations: { + cases: { + aggregations: { + ids: { + terms: { + field: 'cases-comments.references.id', + }, + aggregations: { + reverse: { + reverse_nested: {}, + aggregations: { + topAlerts: { + cardinality: { + field: 'cases-comments.attributes.alertId', + }, + }, + }, + }, + }, + }, + max: { + max_bucket: { + buckets_path: 'ids>reverse.topAlerts', + }, + }, + }, + filter: { + term: { + 'cases-comments.references.type': 'cases', + }, + }, + }, + }, + nested: { + path: 'cases-comments.references', + }, + }, + uniqueAlertCommentsCount: { + cardinality: { + field: 'cases-comments.attributes.alertId', + }, + }, + }, + filter: { + arguments: [ + { + isQuoted: false, + type: 'literal', + value: 'cases-comments.attributes.type', + }, + { + isQuoted: false, + type: 'literal', + value: 'alert', + }, + ], + function: 'is', + type: 'function', + }, + page: 0, + perPage: 0, + type: 'cases-comments', + namespaces: ['*'], + }); + }); + }); + describe('getBucketFromAggregation', () => { it('returns the buckets', () => { expect( diff --git a/x-pack/plugins/cases/server/telemetry/queries/utils.ts b/x-pack/plugins/cases/server/telemetry/queries/utils.ts index 65b81e3362300..6992ed8f7ac06 100644 --- a/x-pack/plugins/cases/server/telemetry/queries/utils.ts +++ b/x-pack/plugins/cases/server/telemetry/queries/utils.ts @@ -27,6 +27,7 @@ import type { FileAttachmentAggsResult, AttachmentFrameworkAggsResult, CustomFieldsTelemetry, + AlertBuckets, } from '../types'; import { buildFilter } from '../../client/utils'; import type { Owner } from '../../../common/constants/types'; @@ -47,6 +48,27 @@ export const getCountsAggregationQuery = (savedObjectType: string) => ({ }, }); +export const getAlertsCountsAggregationQuery = () => ({ + counts: { + date_range: { + field: `${CASE_COMMENT_SAVED_OBJECT}.attributes.created_at`, + format: 'dd/MM/YYYY', + ranges: [ + { from: 'now-1d', to: 'now' }, + { from: 'now-1w', to: 'now' }, + { from: 'now-1M', to: 'now' }, + ], + }, + aggregations: { + topAlertsPerBucket: { + cardinality: { + field: `${CASE_COMMENT_SAVED_OBJECT}.attributes.alertId`, + }, + }, + }, + }, +}); + export const getMaxBucketOnCaseAggregationQuery = (savedObjectType: string) => ({ references: { nested: { @@ -76,6 +98,55 @@ export const getMaxBucketOnCaseAggregationQuery = (savedObjectType: string) => ( }, }); +export const getAlertsMaxBucketOnCaseAggregationQuery = () => ({ + references: { + nested: { + path: `${CASE_COMMENT_SAVED_OBJECT}.references`, + }, + aggregations: { + cases: { + filter: { + term: { + [`${CASE_COMMENT_SAVED_OBJECT}.references.type`]: CASE_SAVED_OBJECT, + }, + }, + aggregations: { + ids: { + terms: { + field: `${CASE_COMMENT_SAVED_OBJECT}.references.id`, + }, + aggregations: { + reverse: { + reverse_nested: {}, + aggregations: { + topAlerts: { + cardinality: { + field: `${CASE_COMMENT_SAVED_OBJECT}.attributes.alertId`, + }, + }, + }, + }, + }, + }, + max: { + max_bucket: { + buckets_path: 'ids>reverse.topAlerts', + }, + }, + }, + }, + }, + }, +}); + +export const getUniqueAlertCommentsCountQuery = () => ({ + uniqueAlertCommentsCount: { + cardinality: { + field: `${CASE_COMMENT_SAVED_OBJECT}.attributes.alertId`, + }, + }, +}); + export const getReferencesAggregationQuery = ({ savedObjectType, referenceType, @@ -121,6 +192,52 @@ export const getCountsFromBuckets = (buckets: Buckets['buckets']) => ({ monthly: buckets?.[0]?.doc_count ?? 0, }); +export const getAlertsCountsFromBuckets = (buckets: AlertBuckets['buckets']) => ({ + daily: buckets?.[2]?.topAlertsPerBucket?.value ?? 0, + weekly: buckets?.[1]?.topAlertsPerBucket?.value ?? 0, + monthly: buckets?.[0]?.topAlertsPerBucket?.value ?? 0, +}); + +export const getCountsAndMaxAlertsData = async ({ + savedObjectsClient, +}: { + savedObjectsClient: TelemetrySavedObjectsClient; +}) => { + const filter = getOnlyAlertsCommentsFilter(); + + const res = await savedObjectsClient.find< + unknown, + { + counts: AlertBuckets; + references: MaxBucketOnCaseAggregation['references']; + uniqueAlertCommentsCount: { value: number }; + } + >({ + page: 0, + perPage: 0, + filter, + type: CASE_COMMENT_SAVED_OBJECT, + namespaces: ['*'], + aggs: { + ...getAlertsCountsAggregationQuery(), + ...getAlertsMaxBucketOnCaseAggregationQuery(), + ...getUniqueAlertCommentsCountQuery(), + }, + }); + + const countsBuckets = res.aggregations?.counts?.buckets ?? []; + const totalAlerts = res.aggregations?.uniqueAlertCommentsCount.value ?? 0; + const maxOnACase = res.aggregations?.references?.cases?.max?.value ?? 0; + + return { + all: { + total: totalAlerts, + ...getAlertsCountsFromBuckets(countsBuckets), + maxOnACase, + }, + }; +}; + export const getCountsAndMaxData = async ({ savedObjectsClient, savedObjectType, @@ -132,7 +249,10 @@ export const getCountsAndMaxData = async ({ }) => { const res = await savedObjectsClient.find< unknown, - { counts: Buckets; references: MaxBucketOnCaseAggregation['references'] } + { + counts: Buckets; + references: MaxBucketOnCaseAggregation['references']; + } >({ page: 0, perPage: 0, diff --git a/x-pack/plugins/cases/server/telemetry/types.ts b/x-pack/plugins/cases/server/telemetry/types.ts index b4996da27f234..228aa0c7ae397 100644 --- a/x-pack/plugins/cases/server/telemetry/types.ts +++ b/x-pack/plugins/cases/server/telemetry/types.ts @@ -17,6 +17,10 @@ export interface Bucket { key: T; } +export interface AlertBuckets { + buckets: Array<{ topAlertsPerBucket: { value: number } }>; +} + export interface Buckets { buckets: Array>; } diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/telemetry.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/telemetry.ts index 0c47e62fae79c..c83210e51e5d5 100644 --- a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/telemetry.ts +++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/telemetry.ts @@ -6,12 +6,14 @@ */ import expect from 'expect'; -import { getPostCaseRequest } from '../../../common/lib/mock'; +import { getPostCaseRequest, postCommentAlertReq } from '../../../common/lib/mock'; import { deleteAllCaseItems, createCase, getTelemetry, runTelemetryTask, + createComment, + bulkCreateAttachments, } from '../../../common/lib/api'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { superUser } from '../../../common/lib/authentication/users'; @@ -49,5 +51,52 @@ export default ({ getService }: FtrProviderContext): void => { expect(res.stats.stack_stats.kibana.plugins.cases.cases.all.total).toBe(2); }); }); + + it('should return the corect total number of alerts attached to cases', async () => { + const firstCase = await createCase(supertest, getPostCaseRequest()); + const secondCase = await createCase(supertest, getPostCaseRequest()); + + const firstCaseAlerts = [...Array(3).keys()].map((num) => `test-case-1-${num}`); + const secondCaseAlerts = [...Array(2).keys()].map((num) => `test-case-2-${num}`); + + await bulkCreateAttachments({ + supertest, + caseId: firstCase.id, + params: [ + { + ...postCommentAlertReq, + alertId: firstCaseAlerts, + index: firstCaseAlerts, + }, + ], + expectedHttpCode: 200, + }); + + await bulkCreateAttachments({ + supertest, + caseId: firstCase.id, + params: [ + { + ...postCommentAlertReq, + alertId: secondCaseAlerts, + index: secondCaseAlerts, + }, + ], + expectedHttpCode: 200, + }); + + await createComment({ + supertest, + caseId: secondCase.id, + params: { ...postCommentAlertReq, alertId: 'test-case-2-3', index: 'test-case-2-3' }, + }); + + await runTelemetryTask(supertest); + + await retry.try(async () => { + const res = await getTelemetry(supertest); + expect(res.stats.stack_stats.kibana.plugins.cases.alerts.all.total).toBe(6); + }); + }); }); }; From a904803e05061076685a2a39ca4c11fb2844b01c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Georgiana-Andreea=20Onolea=C8=9B=C4=83?= Date: Mon, 28 Oct 2024 13:40:19 +0200 Subject: [PATCH 4/9] [ResponseOps][Cases]Add instructions of how to create a connector in the create case form (#197041) Closes https://github.com/elastic/kibana/issues/189246 ## Summary - A helper text was added in the create case form to tell the user that needs to create a connector in the stack management > cases > settings before attaching it to a case - A new "add connector" button was placed in the stack management > cases > settings page, in the connectors section https://github.com/user-attachments/assets/7866b41a-11b5-4ca3-bd65-988412ab1e2f --------- Co-authored-by: Antonio --- .../configure_cases/connectors.test.tsx | 43 +++++++++++++------ .../components/configure_cases/connectors.tsx | 23 ++++++---- .../connectors_dropdown.test.tsx | 26 ----------- .../configure_cases/connectors_dropdown.tsx | 31 +------------ .../components/configure_cases/index.test.tsx | 3 +- .../components/configure_cases/index.tsx | 5 +++ .../configure_cases/translations.ts | 4 ++ .../components/connector_selector/form.tsx | 10 ++++- .../apps/cases/group2/configure.ts | 3 +- .../observability/cases/configure.ts | 3 +- .../security/ftr/cases/configure.ts | 3 +- 11 files changed, 69 insertions(+), 85 deletions(-) diff --git a/x-pack/plugins/cases/public/components/configure_cases/connectors.test.tsx b/x-pack/plugins/cases/public/components/configure_cases/connectors.test.tsx index 0769e7a29cc59..1dc3346a72da6 100644 --- a/x-pack/plugins/cases/public/components/configure_cases/connectors.test.tsx +++ b/x-pack/plugins/cases/public/components/configure_cases/connectors.test.tsx @@ -21,12 +21,20 @@ import { import { ConnectorsDropdown } from './connectors_dropdown'; import { connectors, actionTypes } from './__mock__'; import { ConnectorTypes } from '../../../common/types/domain'; +import userEvent from '@testing-library/user-event'; +import { useApplicationCapabilities } from '../../common/lib/kibana'; + +const useApplicationCapabilitiesMock = useApplicationCapabilities as jest.Mocked< + typeof useApplicationCapabilities +>; +jest.mock('../../common/lib/kibana'); describe('Connectors', () => { let wrapper: ReactWrapper; let appMockRender: AppMockRenderer; const onChangeConnector = jest.fn(); const handleShowEditFlyout = jest.fn(); + const onAddNewConnector = jest.fn(); const props: Props = { actionTypes, @@ -38,6 +46,7 @@ describe('Connectors', () => { onChangeConnector, selectedConnector: { id: 'none', type: ConnectorTypes.none }, updateConnectorDisabled: false, + onAddNewConnector, }; beforeAll(() => { @@ -104,12 +113,16 @@ describe('Connectors', () => { }); it('shows the add connector button', () => { - wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click'); - wrapper.update(); + appMockRender.render(); - expect( - wrapper.find('button[data-test-subj="dropdown-connector-add-connector"]').exists() - ).toBeTruthy(); + expect(screen.getByTestId('add-new-connector')).toBeInTheDocument(); + }); + + it('shows the add connector flyout when the button is clicked', async () => { + appMockRender.render(); + + await userEvent.click(await screen.findByTestId('add-new-connector')); + expect(onAddNewConnector).toHaveBeenCalled(); }); it('the text of the update button is shown correctly', () => { @@ -156,16 +169,14 @@ describe('Connectors', () => { }); it('shows the actions permission message if the user does not have read access to actions', async () => { - appMockRender.coreStart.application.capabilities = { - ...appMockRender.coreStart.application.capabilities, - actions: { save: false, show: false }, - }; + useApplicationCapabilitiesMock().actions = { crud: false, read: false }; + + appMockRender.render(); - const result = appMockRender.render(); expect( - result.getByTestId('configure-case-connector-permissions-error-msg') + await screen.findByTestId('configure-case-connector-permissions-error-msg') ).toBeInTheDocument(); - expect(result.queryByTestId('case-connectors-dropdown')).toBe(null); + expect(screen.queryByTestId('case-connectors-dropdown')).not.toBeInTheDocument(); }); it('shows the actions permission message if the user does not have access to case connector', async () => { @@ -177,4 +188,12 @@ describe('Connectors', () => { ).toBeInTheDocument(); expect(result.queryByTestId('case-connectors-dropdown')).toBe(null); }); + + it('it should hide the "Add Connector" button when the user lacks the capability to add a new connector', () => { + useApplicationCapabilitiesMock().actions = { crud: false, read: true }; + + appMockRender.render(); + + expect(screen.queryByTestId('add-new-connector')).not.toBeInTheDocument(); + }); }); diff --git a/x-pack/plugins/cases/public/components/configure_cases/connectors.tsx b/x-pack/plugins/cases/public/components/configure_cases/connectors.tsx index b1ab16109c28f..3d742a202a0b7 100644 --- a/x-pack/plugins/cases/public/components/configure_cases/connectors.tsx +++ b/x-pack/plugins/cases/public/components/configure_cases/connectors.tsx @@ -13,10 +13,9 @@ import { EuiFlexItem, EuiLink, EuiText, + EuiButtonEmpty, } from '@elastic/eui'; -import { css } from '@emotion/react'; - import { ConnectorsDropdown } from './connectors_dropdown'; import * as i18n from './translations'; @@ -39,6 +38,7 @@ export interface Props { onChangeConnector: (id: string) => void; selectedConnector: { id: string; type: ConnectorTypes }; updateConnectorDisabled: boolean; + onAddNewConnector: () => void; } const ConnectorsComponent: React.FC = ({ actionTypes, @@ -50,8 +50,10 @@ const ConnectorsComponent: React.FC = ({ onChangeConnector, selectedConnector, updateConnectorDisabled, + onAddNewConnector, }) => { const { actions } = useApplicationCapabilities(); + const canSave = actions.crud; const connector = useMemo( () => connectors.find((c) => c.id === selectedConnector.id), [connectors, selectedConnector.id] @@ -95,13 +97,19 @@ const ConnectorsComponent: React.FC = ({ > + {i18n.ADD_CONNECTOR} + + ) : null + } > @@ -113,7 +121,6 @@ const ConnectorsComponent: React.FC = ({ isLoading={isLoading} onChange={onChangeConnector} data-test-subj="case-connectors-dropdown" - appendAddConnectorButton={true} /> ) : ( diff --git a/x-pack/plugins/cases/public/components/configure_cases/connectors_dropdown.test.tsx b/x-pack/plugins/cases/public/components/configure_cases/connectors_dropdown.test.tsx index faabf3f42c70f..30c45453ebc17 100644 --- a/x-pack/plugins/cases/public/components/configure_cases/connectors_dropdown.test.tsx +++ b/x-pack/plugins/cases/public/components/configure_cases/connectors_dropdown.test.tsx @@ -15,13 +15,6 @@ import type { Props } from './connectors_dropdown'; import { ConnectorsDropdown } from './connectors_dropdown'; import { TestProviders } from '../../common/mock'; import { connectors } from './__mock__'; -import userEvent from '@testing-library/user-event'; -import { useApplicationCapabilities } from '../../common/lib/kibana'; - -const useApplicationCapabilitiesMock = useApplicationCapabilities as jest.Mocked< - typeof useApplicationCapabilities ->; -jest.mock('../../common/lib/kibana'); describe('ConnectorsDropdown', () => { let wrapper: ReactWrapper; @@ -388,23 +381,4 @@ describe('ConnectorsDropdown', () => { ); expect(tooltips[0]).toBeInTheDocument(); }); - - test('it should hide the "Add New Connector" button when the user lacks the capability to add a new connector', async () => { - const selectedConnector = 'none'; - useApplicationCapabilitiesMock().actions = { crud: false, read: true }; - render( - {}} - />, - { wrapper: ({ children }) => {children} } - ); - - await userEvent.click(screen.getByTestId('dropdown-connectors')); - expect(screen.queryByTestId('dropdown-connector-add-connector')).not.toBeInTheDocument(); - }); }); diff --git a/x-pack/plugins/cases/public/components/configure_cases/connectors_dropdown.tsx b/x-pack/plugins/cases/public/components/configure_cases/connectors_dropdown.tsx index 71df212399bc2..04fa9e3ef3647 100644 --- a/x-pack/plugins/cases/public/components/configure_cases/connectors_dropdown.tsx +++ b/x-pack/plugins/cases/public/components/configure_cases/connectors_dropdown.tsx @@ -6,7 +6,6 @@ */ import React, { Suspense, useMemo } from 'react'; -import type { EuiThemeComputed } from '@elastic/eui'; import { EuiFlexGroup, EuiFlexItem, @@ -20,7 +19,7 @@ import { css } from '@emotion/react'; import type { ActionConnector } from '../../containers/configure/types'; import * as i18n from './translations'; -import { useApplicationCapabilities, useKibana } from '../../common/lib/kibana'; +import { useKibana } from '../../common/lib/kibana'; import { getConnectorIcon, isDeprecatedConnector } from '../utils'; export interface Props { @@ -29,7 +28,6 @@ export interface Props { isLoading: boolean; onChange: (id: string) => void; selectedConnector: string; - appendAddConnectorButton?: boolean; } const suspendedComponentWithProps = (ComponentToSuspend: React.ComponentType) => { @@ -65,37 +63,14 @@ const noConnectorOption = { 'data-test-subj': 'dropdown-connector-no-connector', }; -const addNewConnector = (euiTheme: EuiThemeComputed<{}>) => ({ - value: 'add-connector', - inputDisplay: ( - - {i18n.ADD_NEW_CONNECTOR} - - ), - 'data-test-subj': 'dropdown-connector-add-connector', -}); - const ConnectorsDropdownComponent: React.FC = ({ connectors, disabled, isLoading, onChange, selectedConnector, - appendAddConnectorButton = false, }) => { const { triggersActionsUi } = useKibana().services; - const { actions } = useApplicationCapabilities(); - const canSave = actions.crud; const { euiTheme } = useEuiTheme(); const connectorsAsOptions = useMemo(() => { const connectorsFormatted = connectors.reduce( @@ -152,10 +127,6 @@ const ConnectorsDropdownComponent: React.FC = ({ [noConnectorOption] ); - if (appendAddConnectorButton && canSave) { - return [...connectorsFormatted, addNewConnector(euiTheme)]; - } - return connectorsFormatted; // eslint-disable-next-line react-hooks/exhaustive-deps }, [connectors]); diff --git a/x-pack/plugins/cases/public/components/configure_cases/index.test.tsx b/x-pack/plugins/cases/public/components/configure_cases/index.test.tsx index 6c65eae41c78b..e058d982e7367 100644 --- a/x-pack/plugins/cases/public/components/configure_cases/index.test.tsx +++ b/x-pack/plugins/cases/public/components/configure_cases/index.test.tsx @@ -565,8 +565,7 @@ describe('ConfigureCases', () => { wrappingComponent: TestProviders as ComponentType>, }); - wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click'); - wrapper.find('button[data-test-subj="dropdown-connector-add-connector"]').simulate('click'); + wrapper.find('button[data-test-subj="add-new-connector"]').simulate('click'); await waitFor(() => { wrapper.update(); diff --git a/x-pack/plugins/cases/public/components/configure_cases/index.tsx b/x-pack/plugins/cases/public/components/configure_cases/index.tsx index 61f99a46a0b08..641482ceca4fe 100644 --- a/x-pack/plugins/cases/public/components/configure_cases/index.tsx +++ b/x-pack/plugins/cases/public/components/configure_cases/index.tsx @@ -215,6 +215,10 @@ export const ConfigureCases: React.FC = React.memo(() => { [] ); + const onAddNewConnector = useCallback(() => { + setFlyOutVisibility({ type: 'addConnector', visible: true }); + }, []); + const onChangeConnector = useCallback( (id: string) => { if (id === 'add-connector') { @@ -577,6 +581,7 @@ export const ConfigureCases: React.FC = React.memo(() => { onChangeConnector={onChangeConnector} selectedConnector={connector} updateConnectorDisabled={updateConnectorDisabled || !permissions.update} + onAddNewConnector={onAddNewConnector} /> diff --git a/x-pack/plugins/cases/public/components/configure_cases/translations.ts b/x-pack/plugins/cases/public/components/configure_cases/translations.ts index 7a2e0e84b0306..4fe462655dcc1 100644 --- a/x-pack/plugins/cases/public/components/configure_cases/translations.ts +++ b/x-pack/plugins/cases/public/components/configure_cases/translations.ts @@ -35,6 +35,10 @@ export const ADD_NEW_CONNECTOR = i18n.translate('xpack.cases.configureCases.addN defaultMessage: 'Add new connector', }); +export const ADD_CONNECTOR = i18n.translate('xpack.cases.configureCases.addConnector', { + defaultMessage: 'Add connector', +}); + export const CASE_CLOSURE_OPTIONS_TITLE = i18n.translate( 'xpack.cases.configureCases.caseClosureOptionsTitle', { diff --git a/x-pack/plugins/cases/public/components/connector_selector/form.tsx b/x-pack/plugins/cases/public/components/connector_selector/form.tsx index fa991bc5b9871..2419aa60b148f 100644 --- a/x-pack/plugins/cases/public/components/connector_selector/form.tsx +++ b/x-pack/plugins/cases/public/components/connector_selector/form.tsx @@ -12,9 +12,17 @@ import { css } from '@emotion/react'; import type { FieldHook } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; import { getFieldValidityAndErrorMessage } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; +import { i18n } from '@kbn/i18n'; import type { ActionConnector } from '../../../common/types/domain'; import { ConnectorsDropdown } from '../configure_cases/connectors_dropdown'; +const ADD_CONNECTOR_HELPER_TEXT = i18n.translate( + 'xpack.cases.connectorSelector.addConnectorHelperText', + { + defaultMessage: 'Go to Cases > Settings to add an external incident management system', + } +); + interface ConnectorSelectorProps { connectors: ActionConnector[]; dataTestSubj: string; @@ -60,7 +68,7 @@ export const ConnectorSelector = ({ describedByIds={idAria ? [idAria] : undefined} error={errorMessage} fullWidth - helpText={field.helpText} + helpText={ADD_CONNECTOR_HELPER_TEXT} isInvalid={isInvalid} label={field.label} labelAppend={field.labelAppend} diff --git a/x-pack/test/functional_with_es_ssl/apps/cases/group2/configure.ts b/x-pack/test/functional_with_es_ssl/apps/cases/group2/configure.ts index ee013b882c487..8b0ade86ac580 100644 --- a/x-pack/test/functional_with_es_ssl/apps/cases/group2/configure.ts +++ b/x-pack/test/functional_with_es_ssl/apps/cases/group2/configure.ts @@ -52,8 +52,7 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { }); it('opens and closes the connectors flyout correctly', async () => { - await common.clickAndValidate('dropdown-connectors', 'dropdown-connector-add-connector'); - await common.clickAndValidate('dropdown-connector-add-connector', 'euiFlyoutCloseButton'); + await common.clickAndValidate('add-new-connector', 'euiFlyoutCloseButton'); await testSubjects.click('euiFlyoutCloseButton'); expect(await testSubjects.exists('euiFlyoutCloseButton')).to.be(false); }); diff --git a/x-pack/test_serverless/functional/test_suites/observability/cases/configure.ts b/x-pack/test_serverless/functional/test_suites/observability/cases/configure.ts index 1909408d05332..1887e76a65e62 100644 --- a/x-pack/test_serverless/functional/test_suites/observability/cases/configure.ts +++ b/x-pack/test_serverless/functional/test_suites/observability/cases/configure.ts @@ -66,8 +66,7 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { }); it('opens and closes the connectors flyout correctly', async () => { - await common.clickAndValidate('dropdown-connectors', 'dropdown-connector-add-connector'); - await common.clickAndValidate('dropdown-connector-add-connector', 'euiFlyoutCloseButton'); + await common.clickAndValidate('add-new-connector', 'euiFlyoutCloseButton'); await testSubjects.click('euiFlyoutCloseButton'); expect(await testSubjects.exists('euiFlyoutCloseButton')).to.be(false); }); diff --git a/x-pack/test_serverless/functional/test_suites/security/ftr/cases/configure.ts b/x-pack/test_serverless/functional/test_suites/security/ftr/cases/configure.ts index e185b4c470548..cc5486a354015 100644 --- a/x-pack/test_serverless/functional/test_suites/security/ftr/cases/configure.ts +++ b/x-pack/test_serverless/functional/test_suites/security/ftr/cases/configure.ts @@ -66,8 +66,7 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { }); it('opens and closes the connectors flyout correctly', async () => { - await common.clickAndValidate('dropdown-connectors', 'dropdown-connector-add-connector'); - await common.clickAndValidate('dropdown-connector-add-connector', 'euiFlyoutCloseButton'); + await common.clickAndValidate('add-new-connector', 'euiFlyoutCloseButton'); await testSubjects.click('euiFlyoutCloseButton'); expect(await testSubjects.exists('euiFlyoutCloseButton')).to.be(false); }); From 8a96f69250ffe674c3d2fff340db39f7f8b99652 Mon Sep 17 00:00:00 2001 From: Julia Rechkunova Date: Mon, 28 Oct 2024 12:45:39 +0100 Subject: [PATCH 5/9] [Discover][ES|QL] Rename Documents tab to Results (#197833) ## Summary This PR renames Documents label to Results for ES|QL mode. Screenshot 2024-10-25 at 15 44 32 Screenshot 2024-10-25 at 15 52 55 ### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --- .../total_documents/total_documents.tsx | 44 ++++++++++++++----- .../view_mode_toggle.test.tsx | 2 + .../view_mode_toggle/view_mode_toggle.tsx | 9 +++- .../components/saved_search_grid.tsx | 4 +- .../translations/translations/fr-FR.json | 1 - .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - 7 files changed, 44 insertions(+), 18 deletions(-) diff --git a/src/plugins/discover/public/application/main/components/total_documents/total_documents.tsx b/src/plugins/discover/public/application/main/components/total_documents/total_documents.tsx index f9a3f04cc1c56..5644efc8a9bbd 100644 --- a/src/plugins/discover/public/application/main/components/total_documents/total_documents.tsx +++ b/src/plugins/discover/public/application/main/components/total_documents/total_documents.tsx @@ -11,7 +11,19 @@ import React from 'react'; import { FormattedMessage, FormattedNumber } from '@kbn/i18n-react'; import { EuiText } from '@elastic/eui'; -export const TotalDocuments = ({ totalHitCount }: { totalHitCount: number }) => { +export const TotalDocuments = ({ + totalHitCount, + isEsqlMode, +}: { + totalHitCount: number; + isEsqlMode?: boolean; +}) => { + const totalDocuments = ( + + + + ); + return ( style={{ paddingRight: 2 }} data-test-subj="savedSearchTotalDocuments" > - - - - ), - }} - /> + {isEsqlMode ? ( + + ) : ( + + )} ); }; diff --git a/src/plugins/discover/public/components/view_mode_toggle/view_mode_toggle.test.tsx b/src/plugins/discover/public/components/view_mode_toggle/view_mode_toggle.test.tsx index f045e78de5ddf..7d88f9ad1fef4 100644 --- a/src/plugins/discover/public/components/view_mode_toggle/view_mode_toggle.test.tsx +++ b/src/plugins/discover/public/components/view_mode_toggle/view_mode_toggle.test.tsx @@ -94,6 +94,7 @@ describe('Document view mode toggle component', () => { expect(findTestSubject(component, 'dscViewModeDocumentButton').exists()).toBe(true); expect(findTestSubject(component, 'dscViewModePatternAnalysisButton').exists()).toBe(true); expect(findTestSubject(component, 'dscViewModeFieldStatsButton').exists()).toBe(true); + expect(findTestSubject(component, 'dscViewModeDocumentButton').text()).toBe('Documents (10)'); }); it('should not render if SHOW_FIELD_STATISTICS is false', async () => { @@ -114,6 +115,7 @@ describe('Document view mode toggle component', () => { expect(findTestSubject(component, 'dscViewModeDocumentButton').exists()).toBe(true); expect(findTestSubject(component, 'dscViewModePatternAnalysisButton').exists()).toBe(false); expect(findTestSubject(component, 'dscViewModeFieldStatsButton').exists()).toBe(true); + expect(findTestSubject(component, 'dscViewModeDocumentButton').text()).toBe('Results (10)'); }); it('should set the view mode to VIEW_MODE.DOCUMENT_LEVEL when dscViewModeDocumentButton is clicked', async () => { diff --git a/src/plugins/discover/public/components/view_mode_toggle/view_mode_toggle.tsx b/src/plugins/discover/public/components/view_mode_toggle/view_mode_toggle.tsx index 10a92dc8fefa9..22c4aaa11b43a 100644 --- a/src/plugins/discover/public/components/view_mode_toggle/view_mode_toggle.tsx +++ b/src/plugins/discover/public/components/view_mode_toggle/view_mode_toggle.tsx @@ -130,7 +130,14 @@ export const DocumentViewModeToggle = ({ onClick={() => setDiscoverViewMode(VIEW_MODE.DOCUMENT_LEVEL)} data-test-subj="dscViewModeDocumentButton" > - + {isEsqlMode ? ( + + ) : ( + + )} diff --git a/src/plugins/discover/public/embeddable/components/saved_search_grid.tsx b/src/plugins/discover/public/embeddable/components/saved_search_grid.tsx index e45ad009db898..f6c77dc6cddf5 100644 --- a/src/plugins/discover/public/embeddable/components/saved_search_grid.tsx +++ b/src/plugins/discover/public/embeddable/components/saved_search_grid.tsx @@ -85,10 +85,10 @@ export function DiscoverGridEmbeddable(props: DiscoverGridEmbeddableProps) { getRenderCustomToolbarWithElements({ leftSide: typeof props.totalHitCount === 'number' ? ( - + ) : undefined, }), - [props.totalHitCount] + [props.totalHitCount, props.isPlainRecord] ); const getCellRenderersAccessor = useProfileAccessor('getCellRenderers'); diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index accc5951d75e0..a0cfae596fc96 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -2423,7 +2423,6 @@ "discover.docTable.tableRow.toggleRowDetailsButtonAriaLabel": "Afficher/Masquer les détails de la ligne", "discover.docTable.tableRow.viewSingleDocumentLinkText": "Afficher un seul document", "discover.docTable.tableRow.viewSurroundingDocumentsLinkText": "Afficher les documents alentour", - "discover.docTable.totalDocuments": "{totalDocuments} documents", "discover.documentsAriaLabel": "Documents", "discover.docViews.table.scoreSortWarningTooltip": "Filtrez sur _score pour pouvoir récupérer les valeurs correspondantes.", "discover.dropZoneTableLabel": "Abandonner la zone pour ajouter un champ en tant que colonne dans la table", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 9d7c444494b6f..6a81bee15918b 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -2422,7 +2422,6 @@ "discover.docTable.tableRow.toggleRowDetailsButtonAriaLabel": "行の詳細を切り替える", "discover.docTable.tableRow.viewSingleDocumentLinkText": "単一のドキュメントを表示", "discover.docTable.tableRow.viewSurroundingDocumentsLinkText": "周りのドキュメントを表示", - "discover.docTable.totalDocuments": "{totalDocuments}ドキュメント", "discover.documentsAriaLabel": "ドキュメント", "discover.docViews.table.scoreSortWarningTooltip": "_scoreの値を取得するには、並べ替える必要があります。", "discover.dropZoneTableLabel": "フィールドを列として表に追加するには、ゾーンをドロップします", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 7c6e7b3e81487..95c94e1f0b17f 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -2424,7 +2424,6 @@ "discover.docTable.tableRow.toggleRowDetailsButtonAriaLabel": "切换行详细信息", "discover.docTable.tableRow.viewSingleDocumentLinkText": "查看单个文档", "discover.docTable.tableRow.viewSurroundingDocumentsLinkText": "查看周围文档", - "discover.docTable.totalDocuments": "{totalDocuments} 个文档", "discover.documentsAriaLabel": "文档", "discover.docViews.table.scoreSortWarningTooltip": "要检索 _score 的值,必须按其筛选。", "discover.dropZoneTableLabel": "放置区域以将字段作为列添加到表中", From 7865045f413533c9685c59df4ae02f6a8875a918 Mon Sep 17 00:00:00 2001 From: Konrad Szwarc Date: Mon, 28 Oct 2024 13:18:28 +0100 Subject: [PATCH 6/9] [EDR Workflows] Enable Blocklist CY in MKI (#197952) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Since adding this test file was tied to changes in Kibana, we initially couldn’t enable it in the MKI. The MKI relies on a Kibana image built from the main branch, and at that time, the necessary changes for these tests to pass hadn’t yet been merged. Now that these updates are included in the main branch, the Kibana image used in MKI has the required changes, so we can proceed with enabling the tests. Manual MKI run - https://buildkite.com/elastic/kibana-serverless-security-solution-quality-gate-defend-workflows/builds/1545 --- .../public/management/cypress/e2e/artifacts/blocklist.cy.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/public/management/cypress/e2e/artifacts/blocklist.cy.ts b/x-pack/plugins/security_solution/public/management/cypress/e2e/artifacts/blocklist.cy.ts index 32dad9b0bbc0d..f0d3eb96e4581 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/e2e/artifacts/blocklist.cy.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/e2e/artifacts/blocklist.cy.ts @@ -41,7 +41,7 @@ const { describe( 'Blocklist', { - tags: ['@ess', '@serverless', '@skipInServerlessMKI'], // @skipInServerlessMKI until kibana is rebuilt after merge + tags: ['@ess', '@serverless'], }, () => { let indexedPolicy: IndexedFleetEndpointPolicyResponse; From 00f34d9b1cf264a21586b77e829340ccf1b960ff Mon Sep 17 00:00:00 2001 From: Vitalii Dmyterko <92328789+vitaliidm@users.noreply.github.com> Date: Mon, 28 Oct 2024 12:36:23 +0000 Subject: [PATCH 7/9] [Security Solution][Detection Engine] removes legacy alerting endpoints from Security Solution dev scripts (#197424) ## Summary - addresses https://github.com/elastic/kibana/issues/95842 --------- Co-authored-by: Ryland Herrick --- .../scripts/{get_alert_types.sh => find_alerting_rules.sh} | 7 ++++--- .../server/lib/detection_engine/scripts/find_rules.sh | 1 + .../{get_alert_instances.sh => get_alerting_rule_types.sh} | 6 +++--- 3 files changed, 8 insertions(+), 6 deletions(-) rename x-pack/plugins/security_solution/server/lib/detection_engine/scripts/{get_alert_types.sh => find_alerting_rules.sh} (59%) rename x-pack/plugins/security_solution/server/lib/detection_engine/scripts/{get_alert_instances.sh => get_alerting_rule_types.sh} (65%) diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/get_alert_types.sh b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/find_alerting_rules.sh similarity index 59% rename from x-pack/plugins/security_solution/server/lib/detection_engine/scripts/get_alert_types.sh rename to x-pack/plugins/security_solution/server/lib/detection_engine/scripts/find_alerting_rules.sh index 9b51c289ac2c3..c735dd333710c 100755 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/get_alert_types.sh +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/find_alerting_rules.sh @@ -10,9 +10,10 @@ set -e ./check_env_variables.sh -# Example: ./get_alert_types.sh -# https://github.com/elastic/kibana/blob/main/x-pack/plugins/alerting/README.md#get-apialerttypes-list-alert-types +# Example: ./find_alerting_rules.sh +# https://www.elastic.co/docs/api/doc/kibana/v8/operation/operation-findrules +# Related: use ./find_rules.sh to retrieve Detection Engine (Security) rules curl -s -k \ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ - -X GET ${KIBANA_URL}${SPACE_URL}/api/alerts/list_alert_types \ + -X GET ${KIBANA_URL}${SPACE_URL}/api/alerting/rules/_find \ | jq . diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/find_rules.sh b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/find_rules.sh index ef8244ad6e200..422f3e2bb0545 100755 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/find_rules.sh +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/find_rules.sh @@ -12,5 +12,6 @@ set -e # Example: ./find_rules.sh curl -s -k \ + -H 'elastic-api-version: 2023-10-31' \ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ -X GET ${KIBANA_URL}${SPACE_URL}/api/detection_engine/rules/_find | jq . diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/get_alert_instances.sh b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/get_alerting_rule_types.sh similarity index 65% rename from x-pack/plugins/security_solution/server/lib/detection_engine/scripts/get_alert_instances.sh rename to x-pack/plugins/security_solution/server/lib/detection_engine/scripts/get_alerting_rule_types.sh index f2ba9bb70a7c6..59c960d67ba4d 100755 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/get_alert_instances.sh +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/get_alerting_rule_types.sh @@ -10,9 +10,9 @@ set -e ./check_env_variables.sh -# Example: ./get_alert_instances.sh -# https://github.com/elastic/kibana/blob/main/x-pack/plugins/alerting/README.md#get-apialert_find-find-alerts +# Example: ./get_rule_types.sh +# https://www.elastic.co/docs/api/doc/kibana/v8/operation/operation-getruletypes curl -s -k \ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ - -X GET ${KIBANA_URL}${SPACE_URL}/api/alerts/_find \ + -X GET ${KIBANA_URL}${SPACE_URL}/api/alerting/rule_types \ | jq . From 9dd4205639ed16f9086a7c5d70e077b6db21d73b Mon Sep 17 00:00:00 2001 From: Elena Shostak <165678770+elena-shostak@users.noreply.github.com> Date: Mon, 28 Oct 2024 13:40:27 +0100 Subject: [PATCH 8/9] [CodeQL] Local run script (#194272) ## Summary This PR introduces a script that allows developers to run CodeQL analysis locally. It uses a Docker container with prebuilt CodeQL queries to facilitate easy setup and execution. The script has the following key steps: - Creating a CodeQL database from the source code. The database is essentially a representation of the codebase that CodeQL uses to analyze for potential issues. - Running the analysis on the created database, `javascript-security-and-quality` suit is used. ### Usage ``` bash scripts/codeql/quick_check.sh -s path/to/your-source-dir ``` For example ``` bash scripts/codeql/quick_check.sh -s ./x-pack/plugins/security_solution/public/common/components/ml/conditional_links ``` The `-s` option allows you to specify the path to the source code directory that you wish to analyze. ### Why custom Docker file? Checked the ability to use MSFT image for local run https://github.com/microsoft/codeql-container. Turned out it has several problems: 1. The published one has an error with [execute permissions](https://github.com/microsoft/codeql-container/issues/53). 2. Container has outdated nodejs version, so it didn't parse our syntax (like `??`) and failed. 3. The technique used in the repository to download the CodeQL binaries and precompile the queries is outdated in the sense that GitHub now offers pre-compiled queries you can just download. Follow this [comment](https://github.com/microsoft/codeql-container/issues/53#issuecomment-1875879512). Taking this into consideration I have created a lightweight docker image without extraneous dependencies for go/.net/java. ## Context and interdependencies issues There are issues sometimes when analyze run returns no results, particularly when analyzing a single folder. It might be due to the missing context for the data flow graph CodeQL generates or context for interdependencies. This is actually a trade off of running it locally for a subset of source directories. We need to explicitly state that in the documentation and advise to expand the scope of source code directories involved for local scan. Documentation for triaging issues will be updated separately. __Closes: https://github.com/elastic/kibana/issues/195740__ --- .gitignore | 3 +- scripts/codeql/codeql.dockerfile | 39 ++++++++++ scripts/codeql/quick_check.sh | 126 +++++++++++++++++++++++++++++++ 3 files changed, 167 insertions(+), 1 deletion(-) create mode 100644 scripts/codeql/codeql.dockerfile create mode 100644 scripts/codeql/quick_check.sh diff --git a/.gitignore b/.gitignore index 7e06f1e23f863..9bda927a92b6a 100644 --- a/.gitignore +++ b/.gitignore @@ -157,4 +157,5 @@ x-pack/test/security_solution_playwright/playwright-report/ x-pack/test/security_solution_playwright/blob-report/ x-pack/test/security_solution_playwright/playwright/.cache/ x-pack/test/security_solution_playwright/.auth/ -x-pack/test/security_solution_playwright/.env \ No newline at end of file +x-pack/test/security_solution_playwright/.env +.codeql diff --git a/scripts/codeql/codeql.dockerfile b/scripts/codeql/codeql.dockerfile new file mode 100644 index 0000000000000..fce6b9c3fdd63 --- /dev/null +++ b/scripts/codeql/codeql.dockerfile @@ -0,0 +1,39 @@ +FROM ubuntu:latest + +ENV DEBIAN_FRONTEND=noninteractive + +ARG USERNAME=codeql +ARG CODEQL_VERSION="v2.19.0" +ENV CODEQL_HOME /usr/local/codeql-home + +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + passwd \ + adduser \ + bash \ + curl \ + git \ + unzip \ + nodejs \ + jq + +RUN adduser --home ${CODEQL_HOME} ${USERNAME} + +RUN curl -Lk "https://github.com/github/codeql-action/releases/download/codeql-bundle-${CODEQL_VERSION}/codeql-bundle-linux64.tar.gz" -o codeql.tar.gz \ + && mkdir -p ${CODEQL_HOME} \ + && tar -xvzf codeql.tar.gz -C ${CODEQL_HOME} \ + && rm codeql.tar.gz + +RUN chmod +x ${CODEQL_HOME}/codeql/codeql + +RUN chown -R ${USERNAME}:${USERNAME} ${CODEQL_HOME} + +USER ${USERNAME} + +ENV PATH="${CODEQL_HOME}/codeql:${PATH}" + +RUN echo $PATH && codeql --version + +WORKDIR /workspace + +ENTRYPOINT ["/bin/bash", "-c"] diff --git a/scripts/codeql/quick_check.sh b/scripts/codeql/quick_check.sh new file mode 100644 index 0000000000000..15023bfb13bfa --- /dev/null +++ b/scripts/codeql/quick_check.sh @@ -0,0 +1,126 @@ +#!/bin/bash + +LANGUAGE="javascript" +CODEQL_DIR=".codeql" +DATABASE_PATH="$CODEQL_DIR/database" +QUERY_OUTPUT="$DATABASE_PATH/results.sarif" +OUTPUT_FORMAT="sarif-latest" +DOCKER_IMAGE="codeql-env" +BASE_DIR="$(cd "$(dirname "$0")"; pwd)" + +# Colors +bold=$(tput bold) +reset=$(tput sgr0) +red=$(tput setaf 1) +green=$(tput setaf 2) +blue=$(tput setaf 4) +yellow=$(tput setaf 3) + +while getopts ":s:r:" opt; do + case $opt in + s) SRC_DIR="$OPTARG" ;; + r) CODEQL_DIR="$OPTARG"; DATABASE_PATH="$CODEQL_DIR/database"; QUERY_OUTPUT="$DATABASE_PATH/results.sarif" ;; + \?) echo "Invalid option -$OPTARG" >&2; exit 1 ;; + :) echo "Option -$OPTARG requires an argument." >&2; exit 1 ;; + esac +done + +if [ -z "$SRC_DIR" ]; then + echo "Usage: $0 -s [-r ]" + exit 1 +fi + +mkdir -p "$CODEQL_DIR" + +# Check the architecture +ARCH=$(uname -m) +PLATFORM_FLAG="" + +# CodeQL CLI binary does not support arm64 architecture, setting the platform to linux/amd64 +if [[ "$ARCH" == "arm64" ]]; then + PLATFORM_FLAG="--platform linux/amd64" +fi + +if [[ "$(docker images -q $DOCKER_IMAGE 2> /dev/null)" == "" ]]; then + echo "Docker image $DOCKER_IMAGE not found. Building locally..." + docker build $PLATFORM_FLAG -t "$DOCKER_IMAGE" -f "$BASE_DIR/codeql.dockerfile" "$BASE_DIR" + if [ $? -ne 0 ]; then + echo "${red}Docker image build failed.${reset}" + exit 1 + fi +fi + +cleanup_database() { + echo "Deleting contents of $CODEQL_DIR." + rm -rf "$CODEQL_DIR"/* +} + +SRC_DIR="$(cd "$(dirname "$SRC_DIR")"; pwd)/$(basename "$SRC_DIR")" +CODEQL_DIR="$(cd "$(dirname "$CODEQL_DIR")"; pwd)/$(basename "$CODEQL_DIR")" +DATABASE_PATH="$(cd "$(dirname "$DATABASE_PATH")"; pwd)/$(basename "$DATABASE_PATH")" + +# Step 1: Run the Docker container to create a CodeQL database from the source code. +echo "Creating a CodeQL database from the source code: $SRC_DIR" +docker run $PLATFORM_FLAG --rm -v "$SRC_DIR":/workspace/source-code \ + -v "${DATABASE_PATH}":/workspace/shared $DOCKER_IMAGE \ + "codeql database create /workspace/shared/codeql-db --language=javascript --source-root=/workspace/source-code --overwrite" + +if [ $? -ne 0 ]; then + echo "CodeQL database creation failed." + cleanup_database + exit 1 +fi + +echo "Analyzing a CodeQL database: $DATABASE_PATH" +# Step 2: Run the Docker container to analyze the CodeQL database. +docker run $PLATFORM_FLAG --rm -v "${DATABASE_PATH}":/workspace/shared $DOCKER_IMAGE \ + "codeql database analyze --format=${OUTPUT_FORMAT} --output=/workspace/shared/results.sarif /workspace/shared/codeql-db javascript-security-and-quality.qls" + +if [ $? -ne 0 ]; then + echo "CodeQL database analysis failed." + cleanup_database + exit 1 +fi + +# Step 3: Print summary of SARIF results +echo "Analysis complete. Results saved to $QUERY_OUTPUT" +if command -v jq &> /dev/null; then + vulnerabilities=$(jq -r '.runs[] | select(.results | length > 0)' "$QUERY_OUTPUT") + + if [[ -z "$vulnerabilities" ]]; then + echo "${blue}${bold}No vulnerabilities found in the SARIF results.${reset}" + else + echo "${yellow}${bold}Summary of SARIF results:${reset}" + jq -r ' + .runs[] | + .results[] as $result | + .tool.driver.rules[] as $rule | + select($rule.id == $result.ruleId) | + "Rule: \($result.ruleId)\nMessage: \($result.message.text)\nFile: \($result.locations[].physicalLocation.artifactLocation.uri)\nLine: \($result.locations[].physicalLocation.region.startLine)\nSecurity Severity: \($rule.properties."security-severity" // "N/A")\n"' "$QUERY_OUTPUT" | + while IFS= read -r line; do + case "$line" in + Rule:*) + echo "${red}${bold}$line${reset}" + ;; + Message:*) + echo "${green}$line${reset}" + ;; + File:*) + echo "${blue}$line${reset}" + ;; + Line:*) + echo "${yellow}$line${reset}" + ;; + Security\ Severity:*) + echo "${yellow}$line${reset}" + ;; + *) + echo "$line" + ;; + esac + done + fi +else + echo "${red}${bold}Please install jq to display a summary of the SARIF results.${reset}" + echo "${bold}You can view the full results in the SARIF file using a SARIF viewer.${reset}" +fi From 60562f37dd2257911030cb4716c63f2048e2ce72 Mon Sep 17 00:00:00 2001 From: Kevin Delemme Date: Mon, 28 Oct 2024 09:01:50 -0400 Subject: [PATCH 9/9] chore(slo): remove tests migrated to agnostic framework (#197711) Resolves https://github.com/elastic/kibana/issues/183397 ## Summary This PR is a follow up of https://github.com/elastic/kibana/pull/195927, that removes the old and migrated tests to the agnostic framework. --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Dzmitry Lemechko --- .buildkite/ftr_oblt_stateful_configs.yml | 1 - .../test/api_integration/apis/slos/config.ts | 29 - .../api_integration/apis/slos/create_slo.ts | 488 ----------- .../api_integration/apis/slos/delete_slo.ts | 141 ---- .../apis/slos/fetch_historical_summary.ts | 134 --- .../test/api_integration/apis/slos/get_slo.ts | 492 ----------- .../test/api_integration/apis/slos/index.ts | 19 - .../api_integration/apis/slos/reset_slo.ts | 93 --- .../api_integration/apis/slos/update_slo.ts | 764 ------------------ .../test_suites/observability/index.ts | 1 - .../observability/slos/create_slo.ts | 371 --------- .../observability/slos/delete_slo.ts | 175 ---- .../slos/fetch_historical_summary.ts | 146 ---- .../test_suites/observability/slos/index.ts | 15 - x-pack/test_serverless/tsconfig.json | 1 - 15 files changed, 2870 deletions(-) delete mode 100644 x-pack/test/api_integration/apis/slos/config.ts delete mode 100644 x-pack/test/api_integration/apis/slos/create_slo.ts delete mode 100644 x-pack/test/api_integration/apis/slos/delete_slo.ts delete mode 100644 x-pack/test/api_integration/apis/slos/fetch_historical_summary.ts delete mode 100644 x-pack/test/api_integration/apis/slos/get_slo.ts delete mode 100644 x-pack/test/api_integration/apis/slos/index.ts delete mode 100644 x-pack/test/api_integration/apis/slos/reset_slo.ts delete mode 100644 x-pack/test/api_integration/apis/slos/update_slo.ts delete mode 100644 x-pack/test_serverless/api_integration/test_suites/observability/slos/create_slo.ts delete mode 100644 x-pack/test_serverless/api_integration/test_suites/observability/slos/delete_slo.ts delete mode 100644 x-pack/test_serverless/api_integration/test_suites/observability/slos/fetch_historical_summary.ts delete mode 100644 x-pack/test_serverless/api_integration/test_suites/observability/slos/index.ts diff --git a/.buildkite/ftr_oblt_stateful_configs.yml b/.buildkite/ftr_oblt_stateful_configs.yml index 6f0cb38be3a62..7655ce6de38cf 100644 --- a/.buildkite/ftr_oblt_stateful_configs.yml +++ b/.buildkite/ftr_oblt_stateful_configs.yml @@ -30,7 +30,6 @@ enabled: - x-pack/test/api_integration/apis/metrics_ui/config.ts - x-pack/test/api_integration/apis/osquery/config.ts - x-pack/test/api_integration/apis/synthetics/config.ts - - x-pack/test/api_integration/apis/slos/config.ts - x-pack/test/api_integration/apis/uptime/config.ts - x-pack/test/api_integration/apis/entity_manager/config.ts - x-pack/test/apm_api_integration/basic/config.ts diff --git a/x-pack/test/api_integration/apis/slos/config.ts b/x-pack/test/api_integration/apis/slos/config.ts deleted file mode 100644 index c755e2a46882d..0000000000000 --- a/x-pack/test/api_integration/apis/slos/config.ts +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { FtrConfigProviderContext } from '@kbn/test'; - -export default async function ({ readConfigFile }: FtrConfigProviderContext) { - const baseIntegrationTestsConfig = await readConfigFile(require.resolve('../../config.ts')); - - return { - ...baseIntegrationTestsConfig.getAll(), - testFiles: [require.resolve('.')], - // overriding default timeouts from packages/kbn-test/src/functional_test_runner/lib/config/schema.ts - // so we can easily adjust them for serverless where needed - timeouts: { - find: 10 * 1000, - try: 120 * 1000, - waitFor: 20 * 1000, - esRequestTimeout: 30 * 1000, - kibanaReportCompletion: 60 * 1000, - kibanaStabilize: 15 * 1000, - navigateStatusPageCheck: 250, - waitForExists: 2500, - }, - }; -} diff --git a/x-pack/test/api_integration/apis/slos/create_slo.ts b/x-pack/test/api_integration/apis/slos/create_slo.ts deleted file mode 100644 index 71ce8434a61f1..0000000000000 --- a/x-pack/test/api_integration/apis/slos/create_slo.ts +++ /dev/null @@ -1,488 +0,0 @@ -/* - * 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 } from '@kbn/infra-forge'; -import type { CreateSLOInput } from '@kbn/slo-schema'; -import { SO_SLO_TYPE } from '@kbn/slo-plugin/server/saved_objects'; - -import { FtrProviderContext } from '../../ftr_provider_context'; -import { sloData } from './fixtures/create_slo'; -import { loadTestData } from './helper/load_test_data'; -import { SloEsClient } from './helper/es'; - -export default function ({ getService }: FtrProviderContext) { - describe('Create SLOs', function () { - this.tags('skipCloud'); - - const supertestAPI = getService('supertest'); - const kibanaServer = getService('kibanaServer'); - const esClient = getService('es'); - const slo = getService('slo'); - const retry = getService('retry'); - const logger = getService('log'); - const sloEsClient = new SloEsClient(esClient); - - let createSLOInput: CreateSLOInput; - - before(async () => { - await slo.createUser(); - await loadTestData(getService); - await slo.deleteAllSLOs(); - }); - - beforeEach(() => { - createSLOInput = sloData; - }); - - afterEach(async () => { - await slo.deleteAllSLOs(); - }); - - after(async () => { - await cleanup({ esClient, logger }); - await sloEsClient.deleteTestSourceData(); - }); - - it('creates a new slo and transforms', async () => { - const apiResponse = await slo.create(createSLOInput); - - expect(apiResponse.body).property('id'); - - const { id } = apiResponse.body; - - const savedObject = await kibanaServer.savedObjects.find({ - type: SO_SLO_TYPE, - }); - - expect(savedObject.saved_objects.length).eql(1); - - expect(savedObject.saved_objects[0].attributes).eql({ - budgetingMethod: 'occurrences', - updatedAt: savedObject.saved_objects[0].attributes.updatedAt, - createdAt: savedObject.saved_objects[0].attributes.createdAt, - description: 'Fixture for api integration tests', - enabled: true, - groupBy: 'tags', - id, - indicator: { - params: { - filter: 'system.network.name: eth1', - good: 'container.cpu.user.pct < 1', - index: 'kbn-data-forge*', - timestampField: '@timestamp', - total: 'container.cpu.user.pct: *', - }, - type: 'sli.kql.custom', - }, - name: 'Test SLO for api integration', - objective: { - target: 0.99, - }, - revision: 1, - settings: { - frequency: '1m', - syncDelay: '1m', - preventInitialBackfill: false, - }, - tags: ['test'], - timeWindow: { - duration: '7d', - type: 'rolling', - }, - version: 2, - }); - - const rollUpTransformResponse = await supertestAPI - .get(`/internal/transform/transforms/slo-${id}-1`) - .set('kbn-xsrf', 'true') - .set('elastic-api-version', '1') - .send() - .expect(200); - - // expect roll up transform to be created - expect(rollUpTransformResponse.body).eql({ - count: 1, - transforms: [ - { - id: `slo-${id}-1`, - authorization: { roles: ['slo_editor', 'editor'] }, - version: '10.0.0', - create_time: rollUpTransformResponse.body.transforms[0].create_time, - source: { - index: ['kbn-data-forge*'], - query: { - bool: { - filter: [ - { range: { '@timestamp': { gte: 'now-7d/d' } } }, - { - bool: { - should: [ - { - match: { - 'system.network.name': 'eth1', - }, - }, - ], - minimum_should_match: 1, - }, - }, - { - exists: { - field: 'tags', - }, - }, - ], - }, - }, - }, - dest: { - index: '.slo-observability.sli-v3.3', - pipeline: `.slo-observability.sli.pipeline-${id}-1`, - }, - frequency: '1m', - sync: { time: { field: '@timestamp', delay: '1m' } }, - pivot: { - group_by: { - 'slo.groupings.tags': { terms: { field: 'tags' } }, - '@timestamp': { date_histogram: { field: '@timestamp', fixed_interval: '1m' } }, - }, - aggregations: { - 'slo.numerator': { - filter: { - bool: { - should: [{ range: { 'container.cpu.user.pct': { lt: '1' } } }], - minimum_should_match: 1, - }, - }, - }, - 'slo.denominator': { - filter: { - bool: { - should: [{ exists: { field: 'container.cpu.user.pct' } }], - minimum_should_match: 1, - }, - }, - }, - }, - }, - description: `Rolled-up SLI data for SLO: Test SLO for api integration [id: ${id}, revision: 1]`, - settings: { deduce_mappings: false, unattended: true }, - _meta: { version: 3.3, managed: true, managed_by: 'observability' }, - }, - ], - }); - - const summaryTransform = await supertestAPI - .get(`/internal/transform/transforms/slo-summary-${id}-1`) - .set('kbn-xsrf', 'true') - .set('elastic-api-version', '1') - .send() - .expect(200); - - // expect summary transform to be created - expect(summaryTransform.body).eql({ - count: 1, - transforms: [ - { - id: `slo-summary-${id}-1`, - authorization: { roles: ['slo_editor', 'editor'] }, - version: '10.0.0', - create_time: summaryTransform.body.transforms[0].create_time, - source: { - index: ['.slo-observability.sli-v3.3*'], - query: { - bool: { - filter: [ - { range: { '@timestamp': { gte: 'now-7d/m', lte: 'now/m' } } }, - { term: { 'slo.id': id } }, - { term: { 'slo.revision': 1 } }, - ], - }, - }, - }, - dest: { - index: '.slo-observability.summary-v3.3', - pipeline: `.slo-observability.summary.pipeline-${id}-1`, - }, - frequency: '1m', - sync: { time: { field: 'event.ingested', delay: '65s' } }, - pivot: { - group_by: { - 'slo.id': { terms: { field: 'slo.id' } }, - 'slo.instanceId': { terms: { field: 'slo.instanceId' } }, - 'slo.revision': { terms: { field: 'slo.revision' } }, - 'slo.groupings.tags': { - terms: { field: 'slo.groupings.tags' }, - }, - 'monitor.config_id': { - terms: { - field: 'monitor.config_id', - missing_bucket: true, - }, - }, - 'monitor.name': { - terms: { - field: 'monitor.name', - missing_bucket: true, - }, - }, - 'observer.geo.name': { - terms: { - field: 'observer.geo.name', - missing_bucket: true, - }, - }, - 'observer.name': { - terms: { - field: 'observer.name', - missing_bucket: true, - }, - }, - 'service.name': { terms: { field: 'service.name', missing_bucket: true } }, - 'service.environment': { - terms: { field: 'service.environment', missing_bucket: true }, - }, - 'transaction.name': { terms: { field: 'transaction.name', missing_bucket: true } }, - 'transaction.type': { terms: { field: 'transaction.type', missing_bucket: true } }, - }, - aggregations: { - goodEvents: { sum: { field: 'slo.numerator' } }, - totalEvents: { sum: { field: 'slo.denominator' } }, - sliValue: { - bucket_script: { - buckets_path: { goodEvents: 'goodEvents', totalEvents: 'totalEvents' }, - script: - 'if (params.totalEvents == 0) { return -1 } else if (params.goodEvents >= params.totalEvents) { return 1 } else { return params.goodEvents / params.totalEvents }', - }, - }, - errorBudgetInitial: { bucket_script: { buckets_path: {}, script: '1 - 0.99' } }, - errorBudgetConsumed: { - bucket_script: { - buckets_path: { - sliValue: 'sliValue', - errorBudgetInitial: 'errorBudgetInitial', - }, - script: - 'if (params.sliValue == -1) { return 0 } else { return (1 - params.sliValue) / params.errorBudgetInitial }', - }, - }, - errorBudgetRemaining: { - bucket_script: { - buckets_path: { errorBudgetConsumed: 'errorBudgetConsumed' }, - script: '1 - params.errorBudgetConsumed', - }, - }, - statusCode: { - bucket_script: { - buckets_path: { - sliValue: 'sliValue', - errorBudgetRemaining: 'errorBudgetRemaining', - }, - script: { - source: - 'if (params.sliValue == -1) { return 0 } else if (params.sliValue >= 0.99) { return 4 } else if (params.errorBudgetRemaining > 0) { return 2 } else { return 1 }', - }, - }, - }, - latestSliTimestamp: { max: { field: '@timestamp' } }, - fiveMinuteBurnRate: { - filter: { - range: { - '@timestamp': { - gte: 'now-480s/m', - lte: 'now-180s/m', - }, - }, - }, - aggs: { - goodEvents: { - sum: { - field: 'slo.numerator', - }, - }, - totalEvents: { - sum: { - field: 'slo.denominator', - }, - }, - }, - }, - oneHourBurnRate: { - filter: { - range: { - '@timestamp': { - gte: 'now-3780s/m', - lte: 'now-180s/m', - }, - }, - }, - aggs: { - goodEvents: { - sum: { - field: 'slo.numerator', - }, - }, - totalEvents: { - sum: { - field: 'slo.denominator', - }, - }, - }, - }, - oneDayBurnRate: { - filter: { - range: { - '@timestamp': { - gte: 'now-86580s/m', - lte: 'now-180s/m', - }, - }, - }, - aggs: { - goodEvents: { - sum: { - field: 'slo.numerator', - }, - }, - totalEvents: { - sum: { - field: 'slo.denominator', - }, - }, - }, - }, - }, - }, - description: `Summarise the rollup data of SLO: Test SLO for api integration [id: ${id}, revision: 1].`, - settings: { deduce_mappings: false, unattended: true }, - _meta: { version: 3.3, managed: true, managed_by: 'observability' }, - }, - ], - }); - }); - - it('creates instanceId for SLOs with multi groupBy', async () => { - createSLOInput.groupBy = ['system.network.name', 'event.dataset']; - - const apiResponse = await slo.create(createSLOInput); - - expect(apiResponse.body).property('id'); - - const { id } = apiResponse.body; - - await retry.tryForTime(300 * 1000, async () => { - const response = await esClient.search(getEsQuery(id)); - - // @ts-ignore - expect(response.aggregations?.last_doc.hits?.hits[0]._source.slo.instanceId).eql( - 'eth1,system.network' - ); - }); - }); - - it('creates instanceId for SLOs with single groupBy', async () => { - createSLOInput.groupBy = 'system.network.name'; - - const apiResponse = await slo.create(createSLOInput); - - expect(apiResponse.body).property('id'); - - const { id } = apiResponse.body; - - await retry.tryForTime(300 * 1000, async () => { - const response = await esClient.search(getEsQuery(id)); - - // @ts-ignore - expect(response.aggregations?.last_doc.hits?.hits[0]._source.slo.instanceId).eql('eth1'); - }); - }); - - it('creates instanceId for SLOs without groupBy ([])', async () => { - createSLOInput.groupBy = []; - - const apiResponse = await slo.create(createSLOInput); - - expect(apiResponse.body).property('id'); - - const { id } = apiResponse.body; - - await retry.tryForTime(300 * 1000, async () => { - const response = await esClient.search(getEsQuery(id)); - - // @ts-ignore - expect(response.aggregations?.last_doc.hits?.hits[0]._source.slo.instanceId).eql('*'); - }); - }); - - it('creates instanceId for SLOs without groupBy (["*"])', async () => { - createSLOInput.groupBy = ['*']; - - const apiResponse = await slo.create(createSLOInput); - - expect(apiResponse.body).property('id'); - - const { id } = apiResponse.body; - - await retry.tryForTime(300 * 1000, async () => { - const response = await esClient.search(getEsQuery(id)); - - // @ts-ignore - expect(response.aggregations?.last_doc.hits?.hits[0]._source.slo.instanceId).eql('*'); - }); - }); - - it('creates instanceId for SLOs without groupBy ("")', async () => { - createSLOInput.groupBy = ''; - - const apiResponse = await slo.create(createSLOInput); - - expect(apiResponse.body).property('id'); - - const { id } = apiResponse.body; - - await retry.tryForTime(300 * 1000, async () => { - const response = await esClient.search(getEsQuery(id)); - - // @ts-ignore - expect(response.aggregations?.last_doc.hits?.hits[0]._source.slo.instanceId).eql('*'); - }); - }); - }); -} - -const getEsQuery = (id: string) => ({ - index: '.slo-observability.sli-v3*', - size: 0, - query: { - bool: { - filter: [ - { - term: { - 'slo.id': id, - }, - }, - ], - }, - }, - aggs: { - last_doc: { - top_hits: { - sort: [ - { - '@timestamp': { - order: 'desc', - }, - }, - ], - _source: { - includes: ['slo.instanceId'], - }, - size: 1, - }, - }, - }, -}); diff --git a/x-pack/test/api_integration/apis/slos/delete_slo.ts b/x-pack/test/api_integration/apis/slos/delete_slo.ts deleted file mode 100644 index 979564f06be55..0000000000000 --- a/x-pack/test/api_integration/apis/slos/delete_slo.ts +++ /dev/null @@ -1,141 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import { cleanup } from '@kbn/infra-forge'; -import expect from '@kbn/expect'; -import type { CreateSLOInput } from '@kbn/slo-schema'; -import { SO_SLO_TYPE } from '@kbn/slo-plugin/server/saved_objects'; - -import { FtrProviderContext } from '../../ftr_provider_context'; -import { sloData } from './fixtures/create_slo'; -import { loadTestData } from './helper/load_test_data'; -import { SloEsClient } from './helper/es'; - -export default function ({ getService }: FtrProviderContext) { - describe('Delete SLOs', function () { - this.tags('skipCloud'); - - const supertestAPI = getService('supertest'); - const kibanaServer = getService('kibanaServer'); - const esClient = getService('es'); - const logger = getService('log'); - const slo = getService('slo'); - const retry = getService('retry'); - const sloEsClient = new SloEsClient(esClient); - - let createSLOInput: CreateSLOInput; - - before(async () => { - await slo.createUser(); - await slo.deleteAllSLOs(); - await sloEsClient.deleteTestSourceData(); - await loadTestData(getService); - }); - - beforeEach(() => { - createSLOInput = sloData; - }); - - afterEach(async () => { - await slo.deleteAllSLOs(); - }); - - after(async () => { - await cleanup({ esClient, logger }); - await sloEsClient.deleteTestSourceData(); - }); - - it('deletes new slo saved object and transforms', async () => { - const response = await slo.create(createSLOInput); - - expect(response.body).property('id'); - - const { id } = response.body; - - await retry.tryForTime(10000, async () => { - const savedObject = await kibanaServer.savedObjects.find({ - type: SO_SLO_TYPE, - }); - - expect(savedObject.saved_objects.length).eql(1); - - expect(savedObject.saved_objects[0].attributes.id).eql(id); - }); - - await retry.tryForTime(300 * 1000, async () => { - // expect summary and rollup data to exist - const sloSummaryResponse = await sloEsClient.getSLOSummaryDataById(id); - const sloRollupResponse = await sloEsClient.getSLORollupDataById(id); - - expect(sloSummaryResponse.hits.hits.length > 0).eql(true); - expect(sloRollupResponse.hits.hits.length > 0).eql(true); - - const rollUpTransformResponse = await supertestAPI - .get(`/internal/transform/transforms/slo-${id}-1`) - .set('kbn-xsrf', 'true') - .set('elastic-api-version', '1') - .send() - .expect(200); - - // expect roll up transform to be created - expect(rollUpTransformResponse.body.transforms[0].id).eql(`slo-${id}-1`); - - const summaryTransform = await supertestAPI - .get(`/internal/transform/transforms/slo-summary-${id}-1`) - .set('kbn-xsrf', 'true') - .set('elastic-api-version', '1') - .send() - .expect(200); - - // expect summary transform to be created - expect(summaryTransform.body.transforms[0].id).eql(`slo-summary-${id}-1`); - - await slo.delete(id); - }); - - // await retry.tryForTime(150 * 1000, async () => { - const savedObjectAfterDelete = await kibanaServer.savedObjects.find({ - type: SO_SLO_TYPE, - }); - - // SO should now be deleted - expect(savedObjectAfterDelete.saved_objects.length).eql(0); - - // roll up transform should be deleted - await supertestAPI - .get(`/internal/transform/transforms/slo-${id}-1`) - .set('kbn-xsrf', 'true') - .set('elastic-api-version', '1') - .send() - .expect(404); - - // summary transform should be deleted - await supertestAPI - .get(`/internal/transform/transforms/slo-summary-${id}-1`) - .set('kbn-xsrf', 'true') - .set('elastic-api-version', '1') - .send() - .expect(404); - - // expect summary and rollup documents to be deleted - await retry.waitForWithTimeout('SLO summary data is deleted', 60 * 1000, async () => { - const sloSummaryResponseAfterDeletion = await sloEsClient.getSLOSummaryDataById(id); - if (sloSummaryResponseAfterDeletion.hits.hits.length > 0) { - throw new Error('SLO summary data not deleted yet'); - } - return true; - }); - - await retry.waitForWithTimeout('SLO rollup data is deleted', 60 * 1000, async () => { - const sloRollupResponseAfterDeletion = await sloEsClient.getSLORollupDataById(id); - if (sloRollupResponseAfterDeletion.hits.hits.length > 1) { - throw new Error('SLO rollup data not deleted yet'); - } - return true; - }); - }); - }); -} diff --git a/x-pack/test/api_integration/apis/slos/fetch_historical_summary.ts b/x-pack/test/api_integration/apis/slos/fetch_historical_summary.ts deleted file mode 100644 index 96f8e21c8c593..0000000000000 --- a/x-pack/test/api_integration/apis/slos/fetch_historical_summary.ts +++ /dev/null @@ -1,134 +0,0 @@ -/* - * 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 { - SLO_DESTINATION_INDEX_NAME, - SLO_DESTINATION_INDEX_PATTERN, -} from '@kbn/slo-plugin/common/constants'; -import { ALL_VALUE } from '@kbn/slo-schema'; -import moment from 'moment'; -import { FtrProviderContext } from '../../ftr_provider_context'; - -export default function ({ getService }: FtrProviderContext) { - const esClient = getService('es'); - const esDeleteAllIndices = getService('esDeleteAllIndices'); - const sloApi = getService('slo'); - - const SLO_ID = 'slo-fake-1'; - - describe('fetch historical summary', () => { - before(async () => { - await sloApi.createUser(); - const now = moment().startOf('minute'); - const curr = now.clone().subtract(30, 'days'); - const end = now.clone().add(5, 'minutes'); - - const batchOperations = []; - while (curr.isSameOrBefore(end)) { - batchOperations.push([ - { index: { _index: SLO_DESTINATION_INDEX_NAME } }, - { - '@timestamp': curr.toISOString(), - slo: { - id: SLO_ID, - revision: 1, - instanceId: ALL_VALUE, - numerator: 90, - denominator: 100, - isGoodSlice: 1, - groupings: {}, - }, - }, - ]); - curr.add(1, 'minute'); - } - - await esClient.bulk({ - index: SLO_DESTINATION_INDEX_NAME, - operations: batchOperations.flat(), - refresh: 'wait_for', - }); - - await esClient.indices.refresh({ index: SLO_DESTINATION_INDEX_NAME }); - }); - - after(async () => { - await esDeleteAllIndices(SLO_DESTINATION_INDEX_PATTERN); - }); - - it('computes the historical summary for a rolling occurrences SLO', async () => { - const response = await sloApi.fetchHistoricalSummary({ - list: [ - { - sloId: SLO_ID, - instanceId: ALL_VALUE, - timeWindow: { - duration: '7d', - type: 'rolling', - }, - budgetingMethod: 'occurrences', - objective: { - target: 0.9, - }, - groupBy: ALL_VALUE, - revision: 1, - }, - ], - }); - expect(response[0].sloId).to.eql(SLO_ID); - expect(response[0].instanceId).to.eql(ALL_VALUE); - const numberOfBuckets = response[0].data.length; - expect(numberOfBuckets).to.be.within(168, 170); // 7 days * 24 hours/day * 1 bucket/hour + 2 extra bucket due to histogram agg rounding - const last = response[0].data.pop(); - expect(last?.errorBudget).to.eql({ - consumed: 1, - initial: 0.1, - isEstimated: false, - remaining: 0, - }); - expect(last?.sliValue).to.eql(0.9); - expect(last?.status).to.eql('HEALTHY'); - }); - - it('computes the historical summary for a rolling timeslices SLO', async () => { - const response = await sloApi.fetchHistoricalSummary({ - list: [ - { - sloId: SLO_ID, - instanceId: ALL_VALUE, - timeWindow: { - duration: '7d', - type: 'rolling', - }, - budgetingMethod: 'timeslices', - objective: { - target: 0.9, - timesliceTarget: 0.8, - timesliceWindow: '1m', - }, - groupBy: ALL_VALUE, - revision: 1, - }, - ], - }); - expect(response[0].sloId).to.eql(SLO_ID); - expect(response[0].instanceId).to.eql(ALL_VALUE); - const numberOfBuckets = response[0].data.length; - expect(numberOfBuckets).to.be.within(168, 170); // 7 days * 24 hours/day * 1 bucket/hour + 2 extra bucket due to histogram agg rounding - const last = response[0].data.pop(); - expect(last?.errorBudget).to.eql({ - consumed: 0, - initial: 0.1, - isEstimated: false, - remaining: 1, - }); - expect(last?.sliValue).to.eql(1); - expect(last?.status).to.eql('HEALTHY'); - }); - }); -} diff --git a/x-pack/test/api_integration/apis/slos/get_slo.ts b/x-pack/test/api_integration/apis/slos/get_slo.ts deleted file mode 100644 index 815409853c7d6..0000000000000 --- a/x-pack/test/api_integration/apis/slos/get_slo.ts +++ /dev/null @@ -1,492 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import { cleanup } from '@kbn/infra-forge'; -import expect from 'expect'; -import type { CreateSLOInput } from '@kbn/slo-schema'; - -import { FtrProviderContext } from '../../ftr_provider_context'; -import { loadTestData } from './helper/load_test_data'; -import { SloEsClient } from './helper/es'; -import { sloData } from './fixtures/create_slo'; - -export const expectSummary = (summary: Record) => { - expect(summary).toEqual({ - sliValue: expect.any(Number), - errorBudget: { - initial: expect.any(Number), - consumed: expect.any(Number), - remaining: expect.any(Number), - isEstimated: expect.any(Boolean), - }, - status: expect.any(String), - fiveMinuteBurnRate: expect.any(Number), - oneDayBurnRate: expect.any(Number), - oneHourBurnRate: expect.any(Number), - }); -}; - -export default function ({ getService }: FtrProviderContext) { - describe('GetSLOs', function () { - this.tags('skipCloud'); - - const supertestAPI = getService('supertest'); - const esClient = getService('es'); - const logger = getService('log'); - const retry = getService('retry'); - const slo = getService('slo'); - // const transform = getService('transform'); - const sloEsClient = new SloEsClient(esClient); - - // const onFailure = async () => { - // const allTransforms = await transform.api.getTransformList(); - // for (const tf of allTransforms.transforms) { - // await transform.api.scheduleTransform(tf.id); - // } - // }; - - let createSLOInput: CreateSLOInput; - - const createSLO = async (requestOverrides?: Record) => { - return await slo.create({ - ...createSLOInput, - ...requestOverrides, - }); - }; - - before(async () => { - await slo.createUser(); - await slo.deleteAllSLOs(); - await sloEsClient.deleteTestSourceData(); - await loadTestData(getService); - }); - - beforeEach(async () => { - createSLOInput = sloData; - }); - - afterEach(async () => { - await retry.tryForTime(60 * 1000, async () => { - await slo.deleteAllSLOs(); - }); - }); - - after(async () => { - await cleanup({ esClient, logger }); - await sloEsClient.deleteTestSourceData(); - }); - - it('gets slo by id and calculates SLI - occurrences rolling', async () => { - const response = await createSLO({ - groupBy: '*', - }); - const id = response.body.id; - - await retry.tryForTime(300 * 1000, async () => { - const getResponse = await supertestAPI - .get(`/api/observability/slos/${id}`) - .set('kbn-xsrf', 'true') - .send() - .expect(200); - - expect(getResponse.body).toEqual({ - name: 'Test SLO for api integration', - description: 'Fixture for api integration tests', - indicator: { - type: 'sli.kql.custom', - params: { - index: 'kbn-data-forge*', - filter: `system.network.name: eth1`, - good: 'container.cpu.user.pct < 1', - total: 'container.cpu.user.pct: *', - timestampField: '@timestamp', - }, - }, - budgetingMethod: 'occurrences', - timeWindow: { duration: '7d', type: 'rolling' }, - objective: { target: 0.99 }, - tags: ['test'], - groupBy: '*', - groupings: {}, - id, - settings: { syncDelay: '1m', frequency: '1m', preventInitialBackfill: false }, - revision: 1, - enabled: true, - createdAt: getResponse.body.createdAt, - updatedAt: getResponse.body.updatedAt, - version: 2, - instanceId: '*', - meta: {}, - summary: expect.any(Object), - }); - expectSummary(getResponse.body.summary); - }); - }); - - it('gets slo by id and calculates SLI - occurrences calendarAligned', async () => { - const response = await createSLO({ - groupBy: '*', - timeWindow: { - duration: '1w', - type: 'calendarAligned', - }, - }); - const id = response.body.id; - - await retry.tryForTime(300 * 1000, async () => { - const getResponse = await supertestAPI - .get(`/api/observability/slos/${id}`) - .set('kbn-xsrf', 'true') - .send() - .expect(200); - - // expect summary transform to be created - expect(getResponse.body).toEqual({ - name: 'Test SLO for api integration', - description: 'Fixture for api integration tests', - indicator: { - type: 'sli.kql.custom', - params: { - index: 'kbn-data-forge*', - filter: `system.network.name: eth1`, - good: 'container.cpu.user.pct < 1', - total: 'container.cpu.user.pct: *', - timestampField: '@timestamp', - }, - }, - budgetingMethod: 'occurrences', - timeWindow: { duration: '1w', type: 'calendarAligned' }, - objective: { target: 0.99 }, - tags: ['test'], - groupBy: '*', - groupings: {}, - id, - settings: { syncDelay: '1m', frequency: '1m', preventInitialBackfill: false }, - revision: 1, - enabled: true, - createdAt: getResponse.body.createdAt, - updatedAt: getResponse.body.updatedAt, - version: 2, - instanceId: '*', - meta: {}, - summary: expect.any(Object), - }); - expectSummary(getResponse.body.summary); - }); - }); - - it('gets slo by id and calculates SLI - timeslices rolling', async () => { - const response = await createSLO({ - groupBy: '*', - timeWindow: { - duration: '7d', - type: 'rolling', - }, - budgetingMethod: 'timeslices', - objective: { - target: 0.99, - timesliceTarget: 0.95, - timesliceWindow: '1m', - }, - }); - const id = response.body.id; - - await retry.tryForTime(300 * 1000, async () => { - const getResponse = await supertestAPI - .get(`/api/observability/slos/${id}`) - .set('kbn-xsrf', 'true') - .send() - .expect(200); - - // expect summary transform to be created - expect(getResponse.body).toEqual({ - name: 'Test SLO for api integration', - description: 'Fixture for api integration tests', - indicator: { - type: 'sli.kql.custom', - params: { - index: 'kbn-data-forge*', - filter: `system.network.name: eth1`, - good: 'container.cpu.user.pct < 1', - total: 'container.cpu.user.pct: *', - timestampField: '@timestamp', - }, - }, - budgetingMethod: 'timeslices', - timeWindow: { duration: '7d', type: 'rolling' }, - objective: { - target: 0.99, - timesliceTarget: 0.95, - timesliceWindow: '1m', - }, - tags: ['test'], - groupBy: '*', - groupings: {}, - id, - settings: { syncDelay: '1m', frequency: '1m', preventInitialBackfill: false }, - revision: 1, - enabled: true, - createdAt: getResponse.body.createdAt, - updatedAt: getResponse.body.updatedAt, - version: 2, - instanceId: '*', - meta: {}, - summary: expect.any(Object), - }); - expectSummary(getResponse.body.summary); - }); - }); - - it('gets slo by id and calculates SLI - timeslices calendarAligned', async () => { - const response = await createSLO({ - groupBy: '*', - timeWindow: { - duration: '1w', - type: 'calendarAligned', - }, - budgetingMethod: 'timeslices', - objective: { - target: 0.99, - timesliceTarget: 0.95, - timesliceWindow: '10m', - }, - }); - const id = response.body.id; - - await retry.tryForTime(300 * 1000, async () => { - const getResponse = await supertestAPI - .get(`/api/observability/slos/${id}`) - .set('kbn-xsrf', 'true') - .send() - .expect(200); - - expect(getResponse.body).toEqual({ - name: 'Test SLO for api integration', - description: 'Fixture for api integration tests', - indicator: { - type: 'sli.kql.custom', - params: { - index: 'kbn-data-forge*', - filter: `system.network.name: eth1`, - good: 'container.cpu.user.pct < 1', - total: 'container.cpu.user.pct: *', - timestampField: '@timestamp', - }, - }, - budgetingMethod: 'timeslices', - timeWindow: { duration: '1w', type: 'calendarAligned' }, - objective: { - target: 0.99, - timesliceTarget: 0.95, - timesliceWindow: '10m', - }, - tags: ['test'], - groupBy: '*', - groupings: {}, - id, - settings: { syncDelay: '1m', frequency: '1m', preventInitialBackfill: false }, - revision: 1, - enabled: true, - createdAt: getResponse.body.createdAt, - updatedAt: getResponse.body.updatedAt, - version: 2, - instanceId: '*', - meta: {}, - summary: expect.any(Object), - }); - expectSummary(getResponse.body.summary); - }); - }); - - it('gets slos by query', async () => { - await createSLO(); - await createSLO({ name: 'test int' }); - - await retry.tryForTime(360 * 1000, async () => { - const response = await supertestAPI - .get(`/api/observability/slos`) - .set('kbn-xsrf', 'true') - .send() - .expect(200); - - expect(response.body.results.length).toEqual(2); - - const searchResponse = await supertestAPI - .get(`/api/observability/slos?kqlQuery=slo.name%3Aapi*`) - .set('kbn-xsrf', 'true') - .send() - .expect(200); - - expect(searchResponse.body.results.length).toEqual(1); - - const searchResponse2 = await supertestAPI - .get(`/api/observability/slos?kqlQuery=slo.name%3Aint`) - .set('kbn-xsrf', 'true') - .send() - .expect(200); - - expect(searchResponse2.body.results.length).toEqual(1); - - const searchResponse3 = await supertestAPI - .get(`/api/observability/slos?kqlQuery=slo.name%3Aint*`) - .set('kbn-xsrf', 'true') - .send() - .expect(200); - - expect(searchResponse3.body.results.length).toEqual(2); - - const searchResponse4 = await supertestAPI - .get(`/api/observability/slos?kqlQuery=int*`) - .set('kbn-xsrf', 'true') - .send() - .expect(200); - - expect(searchResponse4.body.results.length).toEqual(2); - }); - }); - - // not possible for now to reliably fix this - // it.skip('gets slos instances', async () => { - // const createResponse = await createSLO(); - // const id = createResponse.body.id; - // - // await retry.tryForTime( - // 400 * 1000, - // async () => { - // const response = await supertestAPI - // .get(`/api/observability/slos`) - // .set('kbn-xsrf', 'true') - // .send() - // .expect(200); - // const res = response.body.results; - // expect(res.length).toEqual(3); - // const groups = res.map((r: any) => r.groupings.tags); - // - // expect(groups.sort()).toEqual(['1', '2', '3']); - // - // const instanceResponse = await supertestAPI - // .get(`/internal/observability/slos/${id}/_instances`) - // .set('kbn-xsrf', 'true') - // .send() - // .expect(200); - // - // // expect 3 instances to be created - // expect(instanceResponse.body.groupBy).toEqual('tags'); - // expect(instanceResponse.body.instances.sort()).toEqual(['1', '2', '3']); - // }, - // onFailure, - // 10 * 1000 - // ); - // }); - - it('gets slo definitions', async () => { - const createResponse = await createSLO(); - const id = createResponse.body.id; - const secondCreateResponse = await createSLO({ name: 'test name int' }); - const secondId = secondCreateResponse.body.id; - const response = await slo.getDefinitions(); - - expect(response.body).toEqual({ - page: 1, - perPage: 100, - results: [ - { - budgetingMethod: 'occurrences', - createdAt: response.body.results[0].createdAt, - description: 'Fixture for api integration tests', - enabled: true, - groupBy: 'tags', - id, - indicator: { - params: { - filter: 'system.network.name: eth1', - good: 'container.cpu.user.pct < 1', - index: 'kbn-data-forge*', - timestampField: '@timestamp', - total: 'container.cpu.user.pct: *', - }, - type: 'sli.kql.custom', - }, - name: 'Test SLO for api integration', - objective: { - target: 0.99, - }, - revision: 1, - settings: { - frequency: '1m', - syncDelay: '1m', - preventInitialBackfill: false, - }, - tags: ['test'], - timeWindow: { - duration: '7d', - type: 'rolling', - }, - updatedAt: response.body.results[0].updatedAt, - version: 2, - }, - { - budgetingMethod: 'occurrences', - createdAt: response.body.results[1].createdAt, - description: 'Fixture for api integration tests', - enabled: true, - groupBy: 'tags', - id: secondId, - indicator: { - params: { - filter: 'system.network.name: eth1', - good: 'container.cpu.user.pct < 1', - index: 'kbn-data-forge*', - timestampField: '@timestamp', - total: 'container.cpu.user.pct: *', - }, - type: 'sli.kql.custom', - }, - name: 'test name int', - objective: { - target: 0.99, - }, - revision: 1, - settings: { - frequency: '1m', - syncDelay: '1m', - preventInitialBackfill: false, - }, - tags: ['test'], - timeWindow: { - duration: '7d', - type: 'rolling', - }, - updatedAt: response.body.results[1].updatedAt, - version: 2, - }, - ], - total: 2, - }); - - // can search by name - const searchResponse = await slo.getDefinitions({ search: 'api' }); - - expect(searchResponse.body.total).toEqual(1); - - const searchResponse2 = await supertestAPI - .get(`/api/observability/slos/_definitions?search=int`) - .set('kbn-xsrf', 'true') - .send() - .expect(200); - - expect(searchResponse2.body.total).toEqual(1); - - const searchResponse3 = await supertestAPI - .get(`/api/observability/slos/_definitions?search=int*`) - .set('kbn-xsrf', 'true') - .send() - .expect(200); - - expect(searchResponse3.body.total).toEqual(2); - }); - }); -} diff --git a/x-pack/test/api_integration/apis/slos/index.ts b/x-pack/test/api_integration/apis/slos/index.ts deleted file mode 100644 index 3401b195ccee5..0000000000000 --- a/x-pack/test/api_integration/apis/slos/index.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { FtrProviderContext } from '../../ftr_provider_context'; - -export default function ({ loadTestFile }: FtrProviderContext) { - describe('SLO API Tests', () => { - loadTestFile(require.resolve('./get_slo')); - loadTestFile(require.resolve('./create_slo')); - loadTestFile(require.resolve('./delete_slo')); - loadTestFile(require.resolve('./update_slo')); - loadTestFile(require.resolve('./reset_slo')); - loadTestFile(require.resolve('./fetch_historical_summary')); - }); -} diff --git a/x-pack/test/api_integration/apis/slos/reset_slo.ts b/x-pack/test/api_integration/apis/slos/reset_slo.ts deleted file mode 100644 index cccac8f1796be..0000000000000 --- a/x-pack/test/api_integration/apis/slos/reset_slo.ts +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import { cleanup } from '@kbn/infra-forge'; -import expect from '@kbn/expect'; -import { SO_SLO_TYPE } from '@kbn/slo-plugin/server/saved_objects'; - -import { FtrProviderContext } from '../../ftr_provider_context'; -import { loadTestData } from './helper/load_test_data'; -import { SloEsClient } from './helper/es'; - -export default function ({ getService }: FtrProviderContext) { - describe('Reset SLOs', function () { - this.tags('skipCloud'); - - const kibanaServer = getService('kibanaServer'); - const esClient = getService('es'); - const logger = getService('log'); - const slo = getService('slo'); - const sloEsClient = new SloEsClient(esClient); - - before(async () => { - await sloEsClient.deleteTestSourceData(); - await slo.createUser(); - await slo.deleteAllSLOs(); - await loadTestData(getService); - }); - - afterEach(async () => { - await slo.deleteAllSLOs(); - }); - - after(async () => { - await cleanup({ esClient, logger }); - await sloEsClient.deleteTestSourceData(); - }); - - it('updates the SO and transforms', async () => { - // create mock old SLO - const id = 'bdaeccdd-dc63-4138-a1d5-92c075f88087'; - await kibanaServer.savedObjects.clean({ - types: [SO_SLO_TYPE], - }); - await kibanaServer.savedObjects.create({ - type: SO_SLO_TYPE, - overwrite: true, - id, - attributes: { - name: 'Test SLO for api integration', - description: 'Fixture for api integration tests', - indicator: { - type: 'sli.kql.custom', - params: { - index: 'kbn-data-forge*', - filter: 'system.network.name: eth1', - good: 'container.cpu.user.pct < 1', - total: 'container.cpu.user.pct: *', - timestampField: '@timestamp', - }, - }, - budgetingMethod: 'occurrences', - timeWindow: { duration: '7d', type: 'rolling' }, - objective: { target: 0.99 }, - tags: ['test'], - groupBy: '*', - id, - settings: { - syncDelay: '1m', - frequency: '1m', - }, - revision: 1, - enabled: true, - createdAt: '2023-12-14T01:12:35.638Z', - updatedAt: '2023-12-14T01:12:35.638Z', - version: 1, - }, - }); - - const responseBeforeReset = await slo.getDefinitions(); - - expect(responseBeforeReset.body.results[0].version).eql(1); - - await slo.reset(id); - - const responseAfterReset = await slo.getDefinitions(); - - expect(responseAfterReset.body.results[0].version).eql(2); - }); - }); -} diff --git a/x-pack/test/api_integration/apis/slos/update_slo.ts b/x-pack/test/api_integration/apis/slos/update_slo.ts deleted file mode 100644 index a8f4aa1a334f8..0000000000000 --- a/x-pack/test/api_integration/apis/slos/update_slo.ts +++ /dev/null @@ -1,764 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import { cleanup } from '@kbn/infra-forge'; -import expect from '@kbn/expect'; -import type { CreateSLOInput } from '@kbn/slo-schema'; -import { SO_SLO_TYPE } from '@kbn/slo-plugin/server/saved_objects'; - -import { FtrProviderContext } from '../../ftr_provider_context'; -import { loadTestData } from './helper/load_test_data'; -import { sloData } from './fixtures/create_slo'; - -export default function ({ getService }: FtrProviderContext) { - describe('UpdateSLOs', function () { - this.tags('skipCloud'); - - const supertestAPI = getService('supertest'); - const kibanaServer = getService('kibanaServer'); - const esClient = getService('es'); - const logger = getService('log'); - const slo = getService('slo'); - - let createSLOInput: CreateSLOInput; - - before(async () => { - await slo.createUser(); - await slo.deleteAllSLOs(); - await loadTestData(getService); - }); - - beforeEach(() => { - createSLOInput = sloData; - }); - - afterEach(async () => { - await slo.deleteAllSLOs(); - }); - - after(async () => { - await cleanup({ esClient, logger }); - }); - - it('updates the SO and transforms', async () => { - const apiResponse = await supertestAPI - .post('/api/observability/slos') - .set('kbn-xsrf', 'true') - .send(createSLOInput) - .expect(200); - - expect(apiResponse.body).property('id'); - - const { id } = apiResponse.body; - - await supertestAPI - .put(`/api/observability/slos/${id}`) - .set('kbn-xsrf', 'true') - .send({ - ...createSLOInput, - groupBy: 'hosts', - }) - .expect(200); - - const savedObject = await kibanaServer.savedObjects.find({ - type: SO_SLO_TYPE, - }); - - expect(savedObject.saved_objects.length).eql(1); - - expect(savedObject.saved_objects[0].attributes).eql({ - budgetingMethod: 'occurrences', - updatedAt: savedObject.saved_objects[0].attributes.updatedAt, - createdAt: savedObject.saved_objects[0].attributes.createdAt, - description: 'Fixture for api integration tests', - enabled: true, - groupBy: 'hosts', - id, - indicator: { - params: { - filter: 'system.network.name: eth1', - good: 'container.cpu.user.pct < 1', - index: 'kbn-data-forge*', - timestampField: '@timestamp', - total: 'container.cpu.user.pct: *', - }, - type: 'sli.kql.custom', - }, - name: 'Test SLO for api integration', - objective: { - target: 0.99, - }, - revision: 2, - settings: { - frequency: '1m', - syncDelay: '1m', - preventInitialBackfill: false, - }, - tags: ['test'], - timeWindow: { - duration: '7d', - type: 'rolling', - }, - version: 2, - }); - - const rollUpTransformResponse = await supertestAPI - .get(`/internal/transform/transforms/slo-${id}-2`) - .set('kbn-xsrf', 'true') - .set('elastic-api-version', '1') - .send() - .expect(200); - - // expect roll up transform to be created - expect(rollUpTransformResponse.body).eql({ - count: 1, - transforms: [ - { - id: `slo-${id}-2`, - authorization: { roles: ['superuser'] }, - version: '10.0.0', - create_time: rollUpTransformResponse.body.transforms[0].create_time, - source: { - index: ['kbn-data-forge*'], - query: { - bool: { - filter: [ - { range: { '@timestamp': { gte: 'now-7d/d' } } }, - { - bool: { - should: [ - { - match: { - 'system.network.name': 'eth1', - }, - }, - ], - minimum_should_match: 1, - }, - }, - { - exists: { - field: 'hosts', - }, - }, - ], - }, - }, - }, - dest: { - index: '.slo-observability.sli-v3.3', - pipeline: `.slo-observability.sli.pipeline-${id}-2`, - }, - frequency: '1m', - sync: { time: { field: '@timestamp', delay: '1m' } }, - pivot: { - group_by: { - 'slo.groupings.hosts': { terms: { field: 'hosts' } }, - '@timestamp': { date_histogram: { field: '@timestamp', fixed_interval: '1m' } }, - }, - aggregations: { - 'slo.numerator': { - filter: { - bool: { - should: [{ range: { 'container.cpu.user.pct': { lt: '1' } } }], - minimum_should_match: 1, - }, - }, - }, - 'slo.denominator': { - filter: { - bool: { - should: [{ exists: { field: 'container.cpu.user.pct' } }], - minimum_should_match: 1, - }, - }, - }, - }, - }, - description: `Rolled-up SLI data for SLO: Test SLO for api integration [id: ${id}, revision: 2]`, - settings: { deduce_mappings: false, unattended: true }, - _meta: { version: 3.3, managed: true, managed_by: 'observability' }, - }, - ], - }); - - const summaryTransform = await supertestAPI - .get(`/internal/transform/transforms/slo-summary-${id}-2`) - .set('kbn-xsrf', 'true') - .set('elastic-api-version', '1') - .send() - .expect(200); - - // expect summary transform to be created - expect(summaryTransform.body).eql({ - count: 1, - transforms: [ - { - id: `slo-summary-${id}-2`, - authorization: { roles: ['superuser'] }, - version: '10.0.0', - create_time: summaryTransform.body.transforms[0].create_time, - source: { - index: ['.slo-observability.sli-v3.3*'], - query: { - bool: { - filter: [ - { range: { '@timestamp': { gte: 'now-7d/m', lte: 'now/m' } } }, - { term: { 'slo.id': id } }, - { term: { 'slo.revision': 2 } }, - ], - }, - }, - }, - dest: { - index: '.slo-observability.summary-v3.3', - pipeline: `.slo-observability.summary.pipeline-${id}-2`, - }, - frequency: '1m', - sync: { time: { field: 'event.ingested', delay: '65s' } }, - pivot: { - group_by: { - 'slo.id': { terms: { field: 'slo.id' } }, - 'slo.revision': { terms: { field: 'slo.revision' } }, - 'slo.instanceId': { terms: { field: 'slo.instanceId' } }, - 'slo.groupings.hosts': { - terms: { field: 'slo.groupings.hosts' }, - }, - 'monitor.config_id': { - terms: { - field: 'monitor.config_id', - missing_bucket: true, - }, - }, - 'monitor.name': { - terms: { - field: 'monitor.name', - missing_bucket: true, - }, - }, - 'observer.geo.name': { - terms: { - field: 'observer.geo.name', - missing_bucket: true, - }, - }, - 'observer.name': { - terms: { - field: 'observer.name', - missing_bucket: true, - }, - }, - 'service.name': { terms: { field: 'service.name', missing_bucket: true } }, - 'service.environment': { - terms: { field: 'service.environment', missing_bucket: true }, - }, - 'transaction.name': { terms: { field: 'transaction.name', missing_bucket: true } }, - 'transaction.type': { terms: { field: 'transaction.type', missing_bucket: true } }, - }, - aggregations: { - goodEvents: { sum: { field: 'slo.numerator' } }, - totalEvents: { sum: { field: 'slo.denominator' } }, - sliValue: { - bucket_script: { - buckets_path: { goodEvents: 'goodEvents', totalEvents: 'totalEvents' }, - script: - 'if (params.totalEvents == 0) { return -1 } else if (params.goodEvents >= params.totalEvents) { return 1 } else { return params.goodEvents / params.totalEvents }', - }, - }, - errorBudgetInitial: { bucket_script: { buckets_path: {}, script: '1 - 0.99' } }, - errorBudgetConsumed: { - bucket_script: { - buckets_path: { - sliValue: 'sliValue', - errorBudgetInitial: 'errorBudgetInitial', - }, - script: - 'if (params.sliValue == -1) { return 0 } else { return (1 - params.sliValue) / params.errorBudgetInitial }', - }, - }, - errorBudgetRemaining: { - bucket_script: { - buckets_path: { errorBudgetConsumed: 'errorBudgetConsumed' }, - script: '1 - params.errorBudgetConsumed', - }, - }, - statusCode: { - bucket_script: { - buckets_path: { - sliValue: 'sliValue', - errorBudgetRemaining: 'errorBudgetRemaining', - }, - script: { - source: - 'if (params.sliValue == -1) { return 0 } else if (params.sliValue >= 0.99) { return 4 } else if (params.errorBudgetRemaining > 0) { return 2 } else { return 1 }', - }, - }, - }, - latestSliTimestamp: { max: { field: '@timestamp' } }, - fiveMinuteBurnRate: { - filter: { - range: { - '@timestamp': { - gte: 'now-480s/m', - lte: 'now-180s/m', - }, - }, - }, - aggs: { - goodEvents: { - sum: { - field: 'slo.numerator', - }, - }, - totalEvents: { - sum: { - field: 'slo.denominator', - }, - }, - }, - }, - oneHourBurnRate: { - filter: { - range: { - '@timestamp': { - gte: 'now-3780s/m', - lte: 'now-180s/m', - }, - }, - }, - aggs: { - goodEvents: { - sum: { - field: 'slo.numerator', - }, - }, - totalEvents: { - sum: { - field: 'slo.denominator', - }, - }, - }, - }, - oneDayBurnRate: { - filter: { - range: { - '@timestamp': { - gte: 'now-86580s/m', - lte: 'now-180s/m', - }, - }, - }, - aggs: { - goodEvents: { - sum: { - field: 'slo.numerator', - }, - }, - totalEvents: { - sum: { - field: 'slo.denominator', - }, - }, - }, - }, - }, - }, - description: `Summarise the rollup data of SLO: Test SLO for api integration [id: ${id}, revision: 2].`, - settings: { deduce_mappings: false, unattended: true }, - _meta: { version: 3.3, managed: true, managed_by: 'observability' }, - }, - ], - }); - }); - - it('updates an existing slo and does not update transforms when relevant fields are changed', async () => { - const request = createSLOInput; - - const apiResponse = await supertestAPI - .post('/api/observability/slos') - .set('kbn-xsrf', 'true') - .send(request) - .expect(200); - - expect(apiResponse.body).property('id'); - - const { id } = apiResponse.body; - - const savedObject = await kibanaServer.savedObjects.find({ - type: SO_SLO_TYPE, - }); - - expect(savedObject.saved_objects.length).eql(1); - - await supertestAPI - .get(`/internal/transform/transforms/slo-${id}-1`) - .set('kbn-xsrf', 'true') - .set('elastic-api-version', '1') - .send() - .expect(200); - - await supertestAPI - .get(`/internal/transform/transforms/slo-summary-${id}-1`) - .set('kbn-xsrf', 'true') - .set('elastic-api-version', '1') - .send() - .expect(200); - - // change name - await supertestAPI - .put(`/api/observability/slos/${id}`) - .set('kbn-xsrf', 'true') - .send({ - ...request, - name: 'test name', - }) - .expect(200); - - await supertestAPI - .get(`/internal/transform/transforms/slo-${id}-1`) - .set('kbn-xsrf', 'true') - .set('elastic-api-version', '1') - .send() - .expect(200); - - await supertestAPI - .get(`/internal/transform/transforms/slo-summary-${id}-1`) - .set('kbn-xsrf', 'true') - .set('elastic-api-version', '1') - .send() - .expect(200); - - // change description - await supertestAPI - .put(`/api/observability/slos/${id}`) - .set('kbn-xsrf', 'true') - .send({ - ...request, - description: 'test description', - }) - .expect(200); - - await supertestAPI - .get(`/internal/transform/transforms/slo-${id}-1`) - .set('kbn-xsrf', 'true') - .set('elastic-api-version', '1') - .send() - .expect(200); - - await supertestAPI - .get(`/internal/transform/transforms/slo-summary-${id}-1`) - .set('kbn-xsrf', 'true') - .set('elastic-api-version', '1') - .send() - .expect(200); - - // change tags - await supertestAPI - .put(`/api/observability/slos/${id}`) - .set('kbn-xsrf', 'true') - .send({ - ...request, - tags: ['testTag'], - }) - .expect(200); - - await supertestAPI - .get(`/internal/transform/transforms/slo-${id}-1`) - .set('kbn-xsrf', 'true') - .set('elastic-api-version', '1') - .send() - .expect(200); - - await supertestAPI - .get(`/internal/transform/transforms/slo-summary-${id}-1`) - .set('kbn-xsrf', 'true') - .set('elastic-api-version', '1') - .send() - .expect(200); - }); - - it('updates an existing slo and updates transforms when relevant fields are changed', async () => { - const request = createSLOInput; - - const apiResponse = await supertestAPI - .post('/api/observability/slos') - .set('kbn-xsrf', 'true') - .send(request) - .expect(200); - - expect(apiResponse.body).property('id'); - - const { id } = apiResponse.body; - - const savedObject = await kibanaServer.savedObjects.find({ - type: SO_SLO_TYPE, - }); - - expect(savedObject.saved_objects.length).eql(1); - - await supertestAPI - .get(`/internal/transform/transforms/slo-${id}-1`) - .set('kbn-xsrf', 'true') - .set('elastic-api-version', '1') - .send() - .expect(200); - - await supertestAPI - .get(`/internal/transform/transforms/slo-summary-${id}-1`) - .set('kbn-xsrf', 'true') - .set('elastic-api-version', '1') - .send() - .expect(200); - - // change group by - await supertestAPI - .put(`/api/observability/slos/${id}`) - .set('kbn-xsrf', 'true') - .send({ - ...request, - groupBy: 'hosts', - }) - .expect(200); - - await supertestAPI - .get(`/internal/transform/transforms/slo-${id}-1`) - .set('kbn-xsrf', 'true') - .set('elastic-api-version', '1') - .send() - .expect(404); - - await supertestAPI - .get(`/internal/transform/transforms/slo-summary-${id}-1`) - .set('kbn-xsrf', 'true') - .set('elastic-api-version', '1') - .send() - .expect(404); - - await supertestAPI - .get(`/internal/transform/transforms/slo-${id}-2`) - .set('kbn-xsrf', 'true') - .set('elastic-api-version', '1') - .send() - .expect(200); - - await supertestAPI - .get(`/internal/transform/transforms/slo-summary-${id}-2`) - .set('kbn-xsrf', 'true') - .set('elastic-api-version', '1') - .send() - .expect(200); - - // change indicator - await supertestAPI - .put(`/api/observability/slos/${id}`) - .set('kbn-xsrf', 'true') - .send({ - ...request, - indicator: { - ...request.indicator, - params: { - ...request.indicator.params, - index: 'test-index-*', - }, - }, - }) - .expect(200); - - await supertestAPI - .get(`/internal/transform/transforms/slo-${id}-2`) - .set('kbn-xsrf', 'true') - .set('elastic-api-version', '1') - .send() - .expect(404); - - await supertestAPI - .get(`/internal/transform/transforms/slo-summary-${id}-2`) - .set('kbn-xsrf', 'true') - .set('elastic-api-version', '1') - .send() - .expect(404); - - await supertestAPI - .get(`/internal/transform/transforms/slo-${id}-3`) - .set('kbn-xsrf', 'true') - .set('elastic-api-version', '1') - .send() - .expect(200); - - await supertestAPI - .get(`/internal/transform/transforms/slo-summary-${id}-3`) - .set('kbn-xsrf', 'true') - .set('elastic-api-version', '1') - .send() - .expect(200); - - // change time window - await supertestAPI - .put(`/api/observability/slos/${id}`) - .set('kbn-xsrf', 'true') - .send({ - ...request, - timeWindow: { - ...request.timeWindow, - duration: '7d', - }, - }) - .expect(200); - - await supertestAPI - .get(`/internal/transform/transforms/slo-${id}-3`) - .set('kbn-xsrf', 'true') - .set('elastic-api-version', '1') - .send() - .expect(404); - - await supertestAPI - .get(`/internal/transform/transforms/slo-summary-${id}-3`) - .set('kbn-xsrf', 'true') - .set('elastic-api-version', '1') - .send() - .expect(404); - - await supertestAPI - .get(`/internal/transform/transforms/slo-${id}-4`) - .set('kbn-xsrf', 'true') - .set('elastic-api-version', '1') - .send() - .expect(200); - - await supertestAPI - .get(`/internal/transform/transforms/slo-summary-${id}-4`) - .set('kbn-xsrf', 'true') - .set('elastic-api-version', '1') - .send() - .expect(200); - - // change objective - await supertestAPI - .put(`/api/observability/slos/${id}`) - .set('kbn-xsrf', 'true') - .send({ - ...request, - objective: { - target: 0.97, - }, - }) - .expect(200); - - await supertestAPI - .get(`/internal/transform/transforms/slo-${id}-4`) - .set('kbn-xsrf', 'true') - .set('elastic-api-version', '1') - .send() - .expect(404); - - await supertestAPI - .get(`/internal/transform/transforms/slo-summary-${id}-4`) - .set('kbn-xsrf', 'true') - .set('elastic-api-version', '1') - .send() - .expect(404); - - await supertestAPI - .get(`/internal/transform/transforms/slo-${id}-5`) - .set('kbn-xsrf', 'true') - .set('elastic-api-version', '1') - .send() - .expect(200); - - await supertestAPI - .get(`/internal/transform/transforms/slo-summary-${id}-5`) - .set('kbn-xsrf', 'true') - .set('elastic-api-version', '1') - .send() - .expect(200); - - // change budgetingMethod - await supertestAPI - .put(`/api/observability/slos/${id}`) - .set('kbn-xsrf', 'true') - .send({ - ...request, - budgetingMethod: 'timeslices', - objective: { - target: 0.99, - timesliceTarget: 0.95, - timesliceWindow: '1m', - }, - }) - .expect(200); - - await supertestAPI - .get(`/internal/transform/transforms/slo-${id}-5`) - .set('kbn-xsrf', 'true') - .set('elastic-api-version', '1') - .send() - .expect(404); - - await supertestAPI - .get(`/internal/transform/transforms/slo-summary-${id}-5`) - .set('kbn-xsrf', 'true') - .set('elastic-api-version', '1') - .send() - .expect(404); - - await supertestAPI - .get(`/internal/transform/transforms/slo-${id}-6`) - .set('kbn-xsrf', 'true') - .set('elastic-api-version', '1') - .send() - .expect(200); - - await supertestAPI - .get(`/internal/transform/transforms/slo-summary-${id}-6`) - .set('kbn-xsrf', 'true') - .set('elastic-api-version', '1') - .send() - .expect(200); - - // change settings - await supertestAPI - .put(`/api/observability/slos/${id}`) - .set('kbn-xsrf', 'true') - .send({ - ...request, - settings: { - frequency: '2m', - syncDelay: '5m', - }, - }) - .expect(200); - - await supertestAPI - .get(`/internal/transform/transforms/slo-${id}-6`) - .set('kbn-xsrf', 'true') - .set('elastic-api-version', '1') - .send() - .expect(404); - - await supertestAPI - .get(`/internal/transform/transforms/slo-summary-${id}-6`) - .set('kbn-xsrf', 'true') - .set('elastic-api-version', '1') - .send() - .expect(404); - - await supertestAPI - .get(`/internal/transform/transforms/slo-${id}-7`) - .set('kbn-xsrf', 'true') - .set('elastic-api-version', '1') - .send() - .expect(200); - - await supertestAPI - .get(`/internal/transform/transforms/slo-summary-${id}-7`) - .set('kbn-xsrf', 'true') - .set('elastic-api-version', '1') - .send() - .expect(200); - }); - }); -} diff --git a/x-pack/test_serverless/api_integration/test_suites/observability/index.ts b/x-pack/test_serverless/api_integration/test_suites/observability/index.ts index 63f8236a335b6..bfe3fd4cbb2c6 100644 --- a/x-pack/test_serverless/api_integration/test_suites/observability/index.ts +++ b/x-pack/test_serverless/api_integration/test_suites/observability/index.ts @@ -15,7 +15,6 @@ export default function ({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./apm_api_integration/service_maps/service_maps')); loadTestFile(require.resolve('./apm_api_integration/traces/critical_path')); loadTestFile(require.resolve('./cases')); - loadTestFile(require.resolve('./slos')); loadTestFile(require.resolve('./synthetics')); loadTestFile(require.resolve('./dataset_quality_api_integration')); }); diff --git a/x-pack/test_serverless/api_integration/test_suites/observability/slos/create_slo.ts b/x-pack/test_serverless/api_integration/test_suites/observability/slos/create_slo.ts deleted file mode 100644 index 93aaa77e4e215..0000000000000 --- a/x-pack/test_serverless/api_integration/test_suites/observability/slos/create_slo.ts +++ /dev/null @@ -1,371 +0,0 @@ -/* - * 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/infra-forge'; -import expect from '@kbn/expect'; -import type { GetTransformsResponseSchema } from '@kbn/transform-plugin/server/routes/api_schemas/transforms'; -import { SO_SLO_TYPE } from '@kbn/slo-plugin/server/saved_objects'; -import { ALL_VALUE } from '@kbn/slo-schema'; -import { - getSLOPipelineId, - getSLOSummaryPipelineId, - SLO_SUMMARY_TEMP_INDEX_NAME, -} from '@kbn/slo-plugin/common/constants'; -import type { RoleCredentials } from '../../../../shared/services'; -import { FtrProviderContext } from '../../../ftr_provider_context'; - -interface ExpectedTransforms { - count: number; - typeOfVersion: string; - typeOfCreateTime: string; - results: Record; -} - -function assertTransformsResponseBody( - body: GetTransformsResponseSchema, - expectedTransforms: ExpectedTransforms -) { - expect(body.count).to.eql(expectedTransforms.count); - expect(body.transforms).to.have.length(expectedTransforms.count); - - body.transforms.forEach((transform, index) => { - const expectedTransform = expectedTransforms.results[`transform${index}`]; - expect(transform.id).to.eql(expectedTransform.id); - expect(transform.dest.index).to.eql(expectedTransform.destIndex); - expect(typeof transform.version).to.eql(expectedTransforms.typeOfVersion); - expect(typeof transform.create_time).to.eql(expectedTransforms.typeOfCreateTime); - }); -} - -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('create_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 supertest - .delete('/api/observability/slos/my-custom-id3') - .set(svlCommonApi.getInternalRequestHeader()); - - await supertest - .delete('/api/observability/slos/my-custom-id4') - .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('non partition by 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.999, - }, - groupBy: ALL_VALUE, - }, - roleAuthc - ); - }); - - it('saves the SLO definition', async () => { - const savedObject = await kibanaServer.savedObjects.find({ - type: SO_SLO_TYPE, - }); - expect(savedObject.total).to.eql(1); - expect(savedObject.saved_objects[0].attributes.version).eql(2); - expect(savedObject.saved_objects[0].attributes.revision).eql(1); - }); - - it('creates the rollup and summary transforms', async () => { - const expectedTransforms: ExpectedTransforms = { - count: 2, - results: { - transform0: { id: 'slo-my-custom-id1-1', destIndex: '.slo-observability.sli-v3.3' }, - transform1: { - id: 'slo-summary-my-custom-id1-1', - destIndex: '.slo-observability.summary-v3.3', - }, - }, - typeOfVersion: 'string', - typeOfCreateTime: 'number', - }; - const { body, status } = await supertest - .get(`/internal/transform/transforms`) - .set('kbn-xsrf', 'foo') - .set('x-elastic-internal-origin', 'foo') - .set('elastic-api-version', '1') - .set(roleAuthc.apiKeyHeader) - .send(); - transform.api.assertResponseStatusCode(200, status, body); - assertTransformsResponseBody(body, expectedTransforms); - }); - - it('creates ingest pipelines', async () => { - const sloRevision = 1; - const rollupPipelineResponse = await esClient.ingest.getPipeline({ - id: getSLOPipelineId(sloId, sloRevision), - }); - const expectedRollupPipeline = `.slo-observability.sli.pipeline-${sloId}-${sloRevision}`; - expect(rollupPipelineResponse[expectedRollupPipeline]).not.to.be(undefined); - - const summaryPipelineResponse = await esClient.ingest.getPipeline({ - id: getSLOSummaryPipelineId(sloId, sloRevision), - }); - const expectedSummaryPipeline = `.slo-observability.summary.pipeline-${sloId}-${sloRevision}`; - expect(summaryPipelineResponse[expectedSummaryPipeline]).not.to.be(undefined); - expect(summaryPipelineResponse[expectedSummaryPipeline].description).to.be( - `Ingest pipeline for SLO summary data [id: ${sloId}, revision: ${sloRevision}]` - ); - }); - - it('creates summary TEMP index', async () => { - const result = await sloApi.waitForSloSummaryTempIndexToExist(SLO_SUMMARY_TEMP_INDEX_NAME); - expect(result).to.be(true); - }); - - it('finds the created SLO', async () => { - const createdSlo = await sloApi.waitForSloCreated({ - sloId, - roleAuthc, - }); - expect(createdSlo.id).to.be(sloId); - expect(createdSlo.groupBy).to.be(ALL_VALUE); - }); - }); - - describe('SLO with long description', () => { - it('creates an SLO with description over 256 characters', async () => { - const sloId = 'my-custom-id2'; - await sloApi.create( - { - id: sloId, - name: 'my super long SLO name and description', - description: - 'Lorem Ipsum has been the industry standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. ', - 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: '*', - }, - roleAuthc - ); - - const createdSlo = await sloApi.waitForSloCreated({ - sloId, - roleAuthc, - }); - expect(createdSlo.id).to.be(sloId); - }); - }); - - describe('SLO with special characters in the description', () => { - it("creates an SLO that has ' character in the description", async () => { - const sloId = 'my-custom-id3'; - await sloApi.create( - { - id: sloId, - name: 'my SLO with weird characters in the description', - description: - "Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged.", - 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: '*', - }, - roleAuthc - ); - const createdSlo = await sloApi.waitForSloCreated({ - sloId, - roleAuthc, - }); - expect(createdSlo.id).to.be(sloId); - }); - }); - - describe('partition by SLO', () => { - it('creates a partition by SLO', async () => { - const sloId = 'my-custom-id4'; - await sloApi.create( - { - id: sloId, - name: 'Group by SLO', - description: 'This is a group by SLO.', - 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: 'host.name', - }, - roleAuthc - ); - const createdSlo = await sloApi.waitForSloCreated({ - sloId, - roleAuthc, - }); - expect(createdSlo.id).to.be(sloId); - expect(createdSlo.groupBy).not.to.be(ALL_VALUE); - expect(createdSlo.groupBy).to.be('host.name'); - }); - }); - - describe('Total transforms', () => { - it('returns all the transforms for above created SLOs', async () => { - const expectedTransforms: ExpectedTransforms = { - count: 8, - results: { - transform0: { id: 'slo-my-custom-id1-1', destIndex: '.slo-observability.sli-v3.3' }, - transform1: { id: 'slo-my-custom-id2-1', destIndex: '.slo-observability.sli-v3.3' }, - transform2: { id: 'slo-my-custom-id3-1', destIndex: '.slo-observability.sli-v3.3' }, - transform3: { id: 'slo-my-custom-id4-1', destIndex: '.slo-observability.sli-v3.3' }, - transform4: { - id: 'slo-summary-my-custom-id1-1', - destIndex: '.slo-observability.summary-v3.3', - }, - transform5: { - id: 'slo-summary-my-custom-id2-1', - destIndex: '.slo-observability.summary-v3.3', - }, - transform6: { - id: 'slo-summary-my-custom-id3-1', - destIndex: '.slo-observability.summary-v3.3', - }, - transform7: { - id: 'slo-summary-my-custom-id4-1', - destIndex: '.slo-observability.summary-v3.3', - }, - }, - typeOfVersion: 'string', - typeOfCreateTime: 'number', - }; - const { body, status } = await supertest - .get(`/internal/transform/transforms`) - .set('kbn-xsrf', 'foo') - .set('x-elastic-internal-origin', 'foo') - .set('elastic-api-version', '1') - .set(roleAuthc.apiKeyHeader) - .send(); - transform.api.assertResponseStatusCode(200, status, body); - assertTransformsResponseBody(body, expectedTransforms); - }); - }); - - describe('Total SO definitions', () => { - it('returns SO definitions for above created SLOs', async () => { - const savedObject = await kibanaServer.savedObjects.find({ - type: SO_SLO_TYPE, - }); - expect(savedObject.total).to.eql(4); - }); - }); - }); -} diff --git a/x-pack/test_serverless/api_integration/test_suites/observability/slos/delete_slo.ts b/x-pack/test_serverless/api_integration/test_suites/observability/slos/delete_slo.ts deleted file mode 100644 index c33bde45e1720..0000000000000 --- a/x-pack/test_serverless/api_integration/test_suites/observability/slos/delete_slo.ts +++ /dev/null @@ -1,175 +0,0 @@ -/* - * 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 { SO_SLO_TYPE } from '@kbn/slo-plugin/server/saved_objects'; -import { cleanup, generate } from '@kbn/infra-forge'; -import expect from '@kbn/expect'; -import { ALL_VALUE } from '@kbn/slo-schema'; -import { - getSLOSummaryTransformId, - getSLOTransformId, - getSLOSummaryPipelineId, -} from '@kbn/slo-plugin/common/constants'; -import { - SLO_DESTINATION_INDEX_PATTERN, - SLO_SUMMARY_DESTINATION_INDEX_PATTERN, -} from '@kbn/slo-plugin/common/constants'; -import type { RoleCredentials } from '../../../../shared/services'; -import { FtrProviderContext } from '../../../ftr_provider_context'; - -export default function ({ getService }: FtrProviderContext) { - const esClient = getService('es'); - const logger = getService('log'); - const kibanaServer = getService('kibanaServer'); - const sloApi = getService('sloApi'); - const transform = getService('transform'); - const retry = getService('retry'); - - const esDeleteAllIndices = getService('esDeleteAllIndices'); - const dataViewApi = getService('dataViewApi'); - const svlUserManager = getService('svlUserManager'); - - const fetchSloSummaryPipeline = async (sloId: string, sloRevision: number) => { - try { - return await esClient.ingest.getPipeline({ - id: getSLOSummaryPipelineId(sloId, sloRevision), - }); - } catch (error) { - // The GET /_ingest/pipeline API returns an empty object on 404 Not Found. If there are no SLO - // pipelines then return an empty record of pipelines - return {}; - } - }; - - describe('delete_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 sloId: string; - let roleAuthc: RoleCredentials; - - before(async () => { - await sloApi.deleteAllSLOs(); - - 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 sloApi.deleteAllSLOs(); - await esDeleteAllIndices([infraDataIndex]); - await cleanup({ esClient, logger }); - await svlUserManager.invalidateM2mApiKeyWithRoleScope(roleAuthc); - }); - - describe('non partition by SLO', () => { - it('deletes the SLO definition, transforms, ingest pipeline and data', async () => { - const createdSlo = await sloApi.create( - { - 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 - ); - sloId = createdSlo.id; - await sloApi.waitForSloCreated({ sloId, roleAuthc }); - - // Saved Object - const savedObject = await kibanaServer.savedObjects.find({ - type: SO_SLO_TYPE, - }); - expect(savedObject.total).to.eql(1); - expect(savedObject.saved_objects[0].attributes.id).to.eql(sloId); - const sloRevision = savedObject.saved_objects[0].attributes.revision ?? 1; - - // Transforms - const sloTransformId = getSLOTransformId(sloId, sloRevision); - const sloSummaryTransformId = getSLOSummaryTransformId(sloId, sloRevision); - await transform.api.waitForTransformToExist(sloTransformId); - await transform.api.waitForTransformToExist(sloSummaryTransformId); - - // Ingest pipeline - const pipelineResponse = await fetchSloSummaryPipeline(sloId, sloRevision); - expect(pipelineResponse[getSLOSummaryPipelineId(sloId, sloRevision)]).not.to.be(undefined); - - // RollUp and Summary data - const sloRollupData = await sloApi.waitForSloData({ - sloId, - indexName: SLO_DESTINATION_INDEX_PATTERN, - }); - const sloSummaryData = await sloApi.waitForSloData({ - sloId, - indexName: SLO_SUMMARY_DESTINATION_INDEX_PATTERN, - }); - - expect(sloRollupData.hits.hits.length > 0).to.be(true); - expect(sloSummaryData.hits.hits.length > 0).to.be(true); - - // Delete the SLO - const response = await sloApi.waitForSloToBeDeleted({ - sloId, - roleAuthc, - }); - expect(response.status).to.be(204); - - // Saved object definition - const savedObjectAfterDelete = await kibanaServer.savedObjects.find({ - type: SO_SLO_TYPE, - }); - expect(savedObjectAfterDelete.total).to.eql(0); - - // Transforms - await transform.api.getTransform(sloTransformId, 404); - await transform.api.getTransform(sloSummaryTransformId, 404); - - await retry.waitForWithTimeout('SLO summary data is deleted', 60 * 1000, async () => { - const sloSummaryDataAfterDeletion = await sloApi.getSloData({ - sloId, - indexName: SLO_SUMMARY_DESTINATION_INDEX_PATTERN, - }); - if (sloSummaryDataAfterDeletion.hits.hits.length > 0) { - throw new Error('SLO summary data not deleted yet'); - } - return true; - }); - }); - }); - }); -} diff --git a/x-pack/test_serverless/api_integration/test_suites/observability/slos/fetch_historical_summary.ts b/x-pack/test_serverless/api_integration/test_suites/observability/slos/fetch_historical_summary.ts deleted file mode 100644 index 2f8db3098fccf..0000000000000 --- a/x-pack/test_serverless/api_integration/test_suites/observability/slos/fetch_historical_summary.ts +++ /dev/null @@ -1,146 +0,0 @@ -/* - * 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 { - SLO_DESTINATION_INDEX_NAME, - SLO_DESTINATION_INDEX_PATTERN, -} from '@kbn/slo-plugin/common/constants'; - -import { ALL_VALUE } from '@kbn/slo-schema'; -import moment from 'moment'; -import { RoleCredentials } from '../../../../shared/services'; -import { FtrProviderContext } from '../../../ftr_provider_context'; - -export default function ({ getService }: FtrProviderContext) { - const esClient = getService('es'); - const esDeleteAllIndices = getService('esDeleteAllIndices'); - const sloApi = getService('sloApi'); - const svlUserManager = getService('svlUserManager'); - - const SLO_ID = 'slo-fake-1'; - describe('fetch historical summary', () => { - let roleAuthc: RoleCredentials; - - before(async () => { - const now = moment().startOf('minute'); - const curr = now.clone().subtract(30, 'days'); - const end = now.clone().add(5, 'minutes'); - - const batchOperations = []; - - while (curr.isSameOrBefore(end)) { - batchOperations.push([ - { index: { _index: SLO_DESTINATION_INDEX_NAME } }, - { - '@timestamp': curr.toISOString(), - slo: { - id: SLO_ID, - revision: 1, - instanceId: ALL_VALUE, - numerator: 90, - denominator: 100, - isGoodSlice: 1, - groupings: {}, - }, - }, - ]); - curr.add(1, 'minute'); - } - - await esClient.bulk({ - index: SLO_DESTINATION_INDEX_NAME, - operations: batchOperations.flat(), - refresh: 'wait_for', - }); - - await esClient.indices.refresh({ index: SLO_DESTINATION_INDEX_NAME }); - roleAuthc = await svlUserManager.createM2mApiKeyWithRoleScope('admin'); - }); - - after(async () => { - await esDeleteAllIndices(SLO_DESTINATION_INDEX_PATTERN); - await svlUserManager.invalidateM2mApiKeyWithRoleScope(roleAuthc); - }); - - it('computes the historical summary for a rolling occurrences SLO', async () => { - const response = await sloApi.fetchHistoricalSummary( - { - list: [ - { - sloId: SLO_ID, - instanceId: ALL_VALUE, - timeWindow: { - duration: '7d', - type: 'rolling', - }, - budgetingMethod: 'occurrences', - objective: { - target: 0.9, - }, - groupBy: ALL_VALUE, - revision: 1, - }, - ], - }, - roleAuthc - ); - expect(response[0].sloId).to.eql(SLO_ID); - expect(response[0].instanceId).to.eql(ALL_VALUE); - const numberOfBuckets = response[0].data.length; - expect(numberOfBuckets).to.be.within(168, 170); // 7 days * 24 hours/day * 1 bucket/hour + 2 extra bucket due to histogram agg rounding - const last = response[0].data.pop(); - expect(last?.errorBudget).to.eql({ - consumed: 1, - initial: 0.1, - isEstimated: false, - remaining: 0, - }); - expect(last?.sliValue).to.eql(0.9); - expect(last?.status).to.eql('HEALTHY'); - }); - - it('computes the historical summary for a rolling timeslices SLO', async () => { - const response = await sloApi.fetchHistoricalSummary( - { - list: [ - { - sloId: SLO_ID, - instanceId: ALL_VALUE, - timeWindow: { - duration: '7d', - type: 'rolling', - }, - budgetingMethod: 'timeslices', - objective: { - target: 0.9, - timesliceTarget: 0.8, - timesliceWindow: '1m', - }, - groupBy: ALL_VALUE, - revision: 1, - }, - ], - }, - roleAuthc - ); - expect(response[0].sloId).to.eql(SLO_ID); - expect(response[0].instanceId).to.eql(ALL_VALUE); - const numberOfBuckets = response[0].data.length; - expect(numberOfBuckets).to.be.within(168, 170); // 7 days * 24 hours/day * 1 bucket/hour + 2 extra bucket due to histogram agg rounding - const last = response[0].data.pop(); - expect(last?.errorBudget).to.eql({ - consumed: 0, - initial: 0.1, - isEstimated: false, - remaining: 1, - }); - expect(last?.sliValue).to.eql(1); - expect(last?.status).to.eql('HEALTHY'); - }); - }); -} 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 deleted file mode 100644 index 8df59e6f3b624..0000000000000 --- a/x-pack/test_serverless/api_integration/test_suites/observability/slos/index.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import { FtrProviderContext } from '../../../ftr_provider_context'; - -export default function ({ loadTestFile }: FtrProviderContext) { - describe('SLOs', function () { - loadTestFile(require.resolve('./create_slo')); - loadTestFile(require.resolve('./delete_slo')); - loadTestFile(require.resolve('./fetch_historical_summary')); - }); -} diff --git a/x-pack/test_serverless/tsconfig.json b/x-pack/test_serverless/tsconfig.json index 388a4732fdd9e..ed9a78fde0f6f 100644 --- a/x-pack/test_serverless/tsconfig.json +++ b/x-pack/test_serverless/tsconfig.json @@ -78,7 +78,6 @@ "@kbn/ftr-common-functional-ui-services", "@kbn/saved-objects-management-plugin", "@kbn/es", - "@kbn/infra-forge", "@kbn/reporting-common", "@kbn/es-query", "@kbn/slo-plugin",